) => {
15 | const ref = React.useRef(null)
16 | const { tabProps } = useTab(item, state, ref)
17 | const { focusProps, isFocusVisible } = useFocusRing()
18 |
19 | return (
20 |
25 | {item.rendered}
26 |
27 | {isFocusVisible && }
28 |
29 | )
30 | }
31 |
32 | export { Tab }
33 |
--------------------------------------------------------------------------------
/apps/docs/content/components/radio.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Radio button
3 | description: Radio buttons let people select one option from a set of options
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | ------------- | ----------------------- | ----------------------------------------------------------------- | ----------- |
14 | | `value` | `string` | The value of the radio button, used when submitting an HTML form. | `null` |
15 | | `children` | `ReactNode` | The label for the Radio. Accepts any renderable node. | `undefined` |
16 | | `isDisabled` | `boolean` | Whether the radio button is disabled or not. | `undefined` |
17 | | `orientation` | `horizontal` `vertical` | The axis the Radio should align with. | `vertical` |
18 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Pagination/pagination.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | display: flex;
3 | padding: 1rem;
4 | align-items: center;
5 | border-radius: 0.5rem;
6 | justify-content: center;
7 | background-color: var(--md-sys-color-surface);
8 | }
9 | .ul {
10 | list-style: none;
11 | margin: 0;
12 | padding: 0;
13 |
14 | display: none;
15 | gap: 1rem;
16 | }
17 | @media (min-width: 1024px) {
18 | .ul {
19 | display: flex;
20 | }
21 | }
22 | .page-size {
23 | width: 100%;
24 | display: flex;
25 | flex-direction: row;
26 | padding-left: 0.75rem /* 12px */;
27 | padding-right: 0.75rem /* 12px */;
28 | padding-top: 0.25rem /* 4px */;
29 | padding-bottom: 0.25rem /* 4px */;
30 | background-color: var(--md-sys-color-surface);
31 | }
32 | .page-size-wrapper {
33 | display: flex;
34 | margin-left: auto;
35 | flex-direction: row;
36 | align-items: center;
37 | }
38 | .item-per-page {
39 | font-size: 0.875rem /* 14px */;
40 | line-height: 1.25rem /* 20px */;
41 | }
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Label/Label.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { LabelAriaProps } from 'react-aria'
4 | import React from 'react'
5 |
6 | interface LabelProps extends LabelAriaProps, React.ComponentProps<'label'> {
7 | children?: React.ReactNode
8 | }
9 |
10 | const Label = ({ ref, children, ...props }: LabelProps) => {
11 | return (
12 |
27 | )
28 | }
29 |
30 | Label.displayName = 'Actify.Label'
31 |
32 | export { Label }
33 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { motion } from 'motion/react'
5 |
6 | const Logo = ({ height }: React.ComponentProps<'svg'>) => {
7 | return (
8 |
29 | )
30 | }
31 |
32 | export default Logo
33 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/badges.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, Button, Icon, IconButton, Slider } from 'actify'
2 |
3 | import { useState } from 'react'
4 |
5 | export default () => {
6 | const color = 'error'
7 | const [value, setValue] = useState(99)
8 |
9 | const handleChange = (value: number | number[]) => {
10 | setValue(value as number)
11 | }
12 |
13 | return (
14 | <>
15 |
16 |
17 |
18 | person
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 | >
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/radio.tsx:
--------------------------------------------------------------------------------
1 | import { Radio, RadioGroup } from 'actify'
2 |
3 | export default () => {
4 | return (
5 |
6 |
Radio With label
7 |
8 |
9 | Actify
10 | Ngroker
11 | Taildoor
12 | Hugola
13 |
14 |
15 | RadioGroup With label
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/packages/actify/src/components/BottomSheets/BottomSheetsActivator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { Slot } from './../Slot'
5 | import { useBottomSheets } from './BottomSheetsContext'
6 |
7 | export interface BottomSheetsActivatorProps
8 | extends React.ComponentProps<'div'> {
9 | asChild?: boolean
10 | }
11 |
12 | const BottomSheetsActivator = ({
13 | asChild,
14 | className,
15 | children,
16 | ...rest
17 | }: BottomSheetsActivatorProps) => {
18 | const { open, setOpen } = useBottomSheets()
19 |
20 | const handlePress = () => {
21 | setOpen?.(!open)
22 | }
23 |
24 | if (asChild) {
25 | return (
26 |
27 | {children}
28 |
29 | )
30 | }
31 |
32 | return (
33 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export { BottomSheetsActivator }
40 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Field/FilledField.tsx:
--------------------------------------------------------------------------------
1 | import { Field, FieldProps } from './Field'
2 |
3 | import React from 'react'
4 | import styles from './styles/field.module.css'
5 |
6 | interface FilledFieldProps extends FieldProps, React.ComponentProps<'div'> {}
7 |
8 | const renderBackground = () => {
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | const renderIndicator = () =>
17 |
18 | const FilledField = (props: FilledFieldProps) => {
19 | const { children, ...rest } = props
20 |
21 | return (
22 |
28 | {children}
29 |
30 | )
31 | }
32 |
33 | export { FilledField }
34 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/switch.tsx:
--------------------------------------------------------------------------------
1 | import { Label, Switch } from 'actify'
2 |
3 | export default () => {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | label
16 |
20 |
24 |
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/sheets/side-sheets.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | SideSheets,
4 | SideSheetsAction,
5 | SideSheetsActivator,
6 | SideSheetsBody,
7 | SideSheetsContent,
8 | SideSheetsHeader
9 | } from 'actify'
10 |
11 | export default () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | Tittle
19 |
20 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eaque,
21 | deserunt autem maiores reprehenderit mollitia asperiores eius
22 | voluptate voluptatem amet tenetur sapiente sint quasi expedita
23 | repellendus ut eligendi. Tempora, magnam hic.
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Table/TableCell.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AriaTableCellProps,
3 | mergeProps,
4 | useFocusRing,
5 | useTableCell
6 | } from 'react-aria'
7 |
8 | import { FocusRing } from '../FocusRing/FocusRing'
9 | import { GridNode } from '@react-types/grid'
10 | import React from 'react'
11 | import { TableState } from 'react-stately'
12 | import styles from './table.module.css'
13 |
14 | interface TableCellProps extends AriaTableCellProps {
15 | state: TableState
16 | }
17 |
18 | const TableCell = ({ node, state }: TableCellProps) => {
19 | const ref = React.useRef(null)
20 | const { gridCellProps } = useTableCell({ node }, state, ref)
21 | const { isFocusVisible, focusProps } = useFocusRing()
22 |
23 | return (
24 |
29 | {node.rendered}
30 | {isFocusVisible && }
31 | |
32 | )
33 | }
34 |
35 | export { TableCell }
36 |
--------------------------------------------------------------------------------
/apps/docs/content/components/progress/circular-progress.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: CircularProgress
3 | description: Circular progress is a visual indicator that communicates the status of a process.
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Name | Type | Description | Default |
13 | | ----------------- | ----------------------------------------- | ----------------------------------------------- | ----------- |
14 | | `size` | `xs` `sm` `md` `lg` `xl` `2xl` | The size of the circle | `sm` |
15 | | `value` | `number` | set the value of progress 0-100 | `undefined` |
16 | | `color` | `primary` `secondary` `tertiaray` `error` | set the color of progress circle | `primary` |
17 | | `isIndeterminate` | `boolean` | whether determinate circular progress indicator | `false` |
18 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Dialogs/DialogActivator.tsx:
--------------------------------------------------------------------------------
1 | import { OverlayTriggerProps, useOverlayTriggerState } from 'react-stately'
2 |
3 | import { Button } from '../Buttons'
4 | import { Modal } from '../Modal'
5 | import React from 'react'
6 | import { useOverlayTrigger } from 'react-aria'
7 |
8 | interface DialogActivatorProps extends OverlayTriggerProps {
9 | label: string
10 | children: (close: () => void) => React.JSX.Element
11 | }
12 |
13 | const DialogActivator = ({
14 | label,
15 | children,
16 | ...props
17 | }: DialogActivatorProps) => {
18 | const state = useOverlayTriggerState(props)
19 | const { triggerProps, overlayProps } = useOverlayTrigger(
20 | {
21 | type: 'dialog'
22 | },
23 | state
24 | )
25 |
26 | return (
27 | <>
28 |
29 | {state.isOpen && (
30 |
31 | {React.cloneElement(children(state.close), overlayProps)}
32 |
33 | )}
34 | >
35 | )
36 | }
37 |
38 | export { DialogActivator }
39 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Divider/Divider.tsx:
--------------------------------------------------------------------------------
1 | import { SeparatorProps, useSeparator } from 'react-aria'
2 |
3 | import React from 'react'
4 | import { StyleProps } from '../../utils'
5 | import clsx from 'clsx'
6 | import styles from './divider.module.css'
7 |
8 | interface DividerProps extends SeparatorProps, StyleProps {
9 | inset?: boolean
10 | insetStart?: boolean
11 | insetEnd?: boolean
12 | }
13 | const Divider = (props: DividerProps) => {
14 | const {
15 | className,
16 | inset,
17 | insetStart,
18 | insetEnd,
19 | orientation = 'horizontal'
20 | } = props
21 |
22 | const { separatorProps } = useSeparator(props)
23 |
24 | return (
25 |
36 | )
37 | }
38 |
39 | export { Divider }
40 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Item/Item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styles from './item.module.css'
3 |
4 | export interface ItemProps extends React.ComponentProps<'div'> {
5 | container?: React.ReactNode
6 | start?: React.ReactNode
7 | overline?: React.ReactNode
8 | headline?: React.ReactNode
9 | supportingText?: React.ReactNode
10 | trailingSupportingText?: React.ReactNode
11 | end?: React.ReactNode
12 | }
13 | const Item = (props: ItemProps) => {
14 | const {
15 | container,
16 | start,
17 | overline,
18 | headline,
19 | supportingText,
20 | trailingSupportingText,
21 | end,
22 | children,
23 | ...rest
24 | } = props
25 | return (
26 |
27 | {container}
28 | {start}
29 | {end}
30 | {children}
31 | {overline}
32 | {headline}
33 | {supportingText}
34 | {trailingSupportingText}
35 |
36 | )
37 | }
38 |
39 | Item.displayName = 'Actify.Item'
40 |
41 | export { Item }
42 |
--------------------------------------------------------------------------------
/packages/actify/src/components/ListBox/ListBox.tsx:
--------------------------------------------------------------------------------
1 | import { AriaListBoxOptions, useListBox } from 'react-aria'
2 |
3 | import { ListBoxOption } from './ListBoxOption'
4 | import { ListState } from 'react-stately'
5 | import React from 'react'
6 | import styles from './listbox.module.css'
7 |
8 | interface ListBoxProps extends AriaListBoxOptions {
9 | state: ListState
10 | listBoxProps?: AriaListBoxOptions
11 | listBoxRef?: React.RefObject
12 | }
13 |
14 | const ListBox = (props: ListBoxProps) => {
15 | const ref = React.useRef(null)
16 | const { listBoxRef = ref, state } = props
17 | const { listBoxProps } = useListBox(props, state, listBoxRef)
18 |
19 | return (
20 | }
24 | >
25 | {[...state.collection].map((item) => (
26 |
27 | ))}
28 |
29 | )
30 | }
31 |
32 | export { ListBox }
33 |
--------------------------------------------------------------------------------
/apps/docs/content/components/lists.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Lists
3 | description: Lists are continuous, vertical indexes of text and images
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Name | Type | Description | Default |
13 | | ------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------- | ------- |
14 | | `transition` | `Object` | Item hover trantisition is framer motion transition | `null` |
15 | | `children` | `React.ReactNode` | children | `null` |
16 |
17 | ## Child
18 |
19 | | Name | Description |
20 | | --------- | ----------- |
21 | | ListItem | List item |
22 | | ListGroup | List group |
23 |
--------------------------------------------------------------------------------
/apps/docs/content/components/cards.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cards
3 | description: Cards display content and actions about a single subject
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## With Ripple
11 |
12 |
26 |
27 | ## Props
28 |
29 | | Props | Type | Description | Default |
30 | | ------ | ------------------- | --------------------- | ------- |
31 | | `type` | `filled` `outlined` | The type of the Card. | null |
32 |
--------------------------------------------------------------------------------
/packages/actify/src/components/NavigationDrawer/DrawerActivator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { cloneElement, isValidElement } from 'react'
4 |
5 | import { useDrawer } from './DrawerContext'
6 |
7 | export interface DrawerActivatorProps extends React.ComponentProps<'div'> {
8 | asChild?: boolean
9 | }
10 |
11 | const DrawerActivator = (props: DrawerActivatorProps) => {
12 | const { setOpen } = useDrawer()
13 | const { ref, className, asChild, children, ...rest } = props
14 |
15 | const handlePress = () => {
16 | setOpen?.(true)
17 | }
18 |
19 | // `asChild` allows the user to pass any element as the activator
20 | if (asChild && isValidElement(children)) {
21 | return cloneElement(children, {
22 | ref,
23 | ...rest,
24 | // @ts-ignore
25 | ...children.props,
26 | role: 'button',
27 | onPress: handlePress
28 | })
29 | }
30 |
31 | return (
32 |
33 | {children}
34 |
35 | )
36 | }
37 |
38 | export { DrawerActivator }
39 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Badges/Badges.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useMemo } from 'react'
4 |
5 | import clsx from 'clsx'
6 | import styles from './badges.module.css'
7 |
8 | interface BadgeProps extends React.ComponentProps<'div'> {
9 | value?: string | number
10 | color?: 'primary' | 'secondary' | 'tertiary' | 'error'
11 | }
12 |
13 | const Badge = (props: BadgeProps) => {
14 | const { color = 'error', value, className, style, children, ...rest } = props
15 |
16 | const badge = useMemo(() => {
17 | if (!value) return false
18 | if (parseInt(value.toString()) > 999) {
19 | return '999+'
20 | } else {
21 | return value
22 | }
23 | }, [value])
24 |
25 | const classes = clsx(styles['badge'], styles[color])
26 |
27 | return (
28 |
29 | {children}
30 | {badge && (
31 |
32 | {badge}
33 |
34 | )}
35 |
36 | )
37 | }
38 |
39 | export { Badge }
40 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Table/TableSelectAllCell.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AriaTableCellProps,
3 | VisuallyHidden,
4 | useTableColumnHeader,
5 | useTableSelectAllCheckbox
6 | } from 'react-aria'
7 |
8 | import { Checkbox } from '../Checkbox'
9 | import React from 'react'
10 | import { TableState } from 'react-stately'
11 |
12 | interface TableSelectAllCellProps extends AriaTableCellProps {
13 | state: TableState
14 | }
15 | const TableSelectAllCell = ({
16 | node,
17 | state
18 | }: TableSelectAllCellProps) => {
19 | const ref = React.useRef(null)
20 | const { columnHeaderProps } = useTableColumnHeader({ node }, state, ref)
21 | const { checkboxProps } = useTableSelectAllCheckbox(state)
22 |
23 | return (
24 |
25 | {state.selectionManager.selectionMode === 'single' ? (
26 | {checkboxProps['aria-label']}
27 | ) : (
28 |
29 | )}
30 | |
31 | )
32 | }
33 |
34 | export { TableSelectAllCell }
35 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { AriaModalOverlayProps, Overlay, useModalOverlay } from 'react-aria'
2 |
3 | import { OverlayTriggerState } from 'react-stately'
4 | import React from 'react'
5 | import styles from './modal.module.css'
6 |
7 | interface ModalProps extends AriaModalOverlayProps {
8 | children: React.ReactNode
9 | state: OverlayTriggerState
10 | }
11 | export function Modal({ state, children, ...props }: ModalProps) {
12 | const ref = React.useRef(null)
13 | const { modalProps, underlayProps } = useModalOverlay(props, state, ref)
14 | const [exited, setExited] = React.useState(!state.isOpen)
15 |
16 | // Don't render anything if the modal is not open and we're not animating out.
17 | if (!(state.isOpen || !exited)) {
18 | return null
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/docs/content/components/before-after.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Before After
3 | description: Before after show a before and after image of a product.
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | --------- | ----- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
14 | | `before` | `url` | original image url | `null` |
15 | | `after` | `url` | after image url | `null` |
16 | | `bgImage` | `url` | after image background image | |
17 |
--------------------------------------------------------------------------------
/apps/docs/content/getting-started/why-actify.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Why Actify
3 | ---
4 |
5 | ## What is Actify?
6 |
7 | Actify is an open source react component library written in Vite + React + Tailwind CSS based on Google's [Material Design 3](https://m3.material.io/). Highly inspired by [Vuetify](https://www.vuetifyjs.com). You can use it for write react pages.
8 |
9 | ## Getting started
10 |
11 | For a complete list of installation options please navigate to the [Installation page](/getting-started/installation)
12 |
13 | ## Why Actify?
14 |
15 | Actify is a powerful React Component Library built from the ground up to be easy to learn and rewarding to master. Our collection of UI components maintain a consistent style throughout your application with enough customization options to meet any use-case.
16 |
17 | ## It’s free
18 |
19 | Actify is an Open Source project available for free under the [MIT licensed](http://opensource.org/licenses/MIT)
20 | . Additionally, Actify’s source code is available on GitHub, allowing developers to modify and contribute to its development if they choose to do so.
21 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Buttons/fab.module.css:
--------------------------------------------------------------------------------
1 | .fab {
2 | border: 0 solid #e5e7eb;
3 | appearance: button;
4 | background-color: transparent;
5 | background-image: none;
6 | font-feature-settings: inherit;
7 | font-variation-settings: inherit;
8 | letter-spacing: inherit;
9 | color: inherit;
10 | padding: 0;
11 | cursor: pointer;
12 |
13 | width: fit-content;
14 | position: relative;
15 | display: inline-flex;
16 | align-items: center;
17 | justify-content: center;
18 | }
19 | .fab:focus-visible {
20 | outline: none;
21 | }
22 | .small {
23 | height: 2.5rem;
24 | padding-left: 0.5rem;
25 | padding-right: 0.5rem;
26 | border-radius: 0.75rem /* 12px */;
27 | --md-focus-ring-shape: 0.75rem;
28 | }
29 |
30 | .medium {
31 | height: 3.5rem /* 56px */;
32 | padding-left: 1rem;
33 | padding-right: 1rem;
34 | border-radius: 1rem /* 16px */;
35 | --md-focus-ring-shape: 1rem;
36 | }
37 |
38 | .large {
39 | height: 6rem /* 96px */;
40 | padding-left: 2rem;
41 | padding-right: 2rem;
42 | border-radius: 1.75rem /* 12px */;
43 | --md-focus-ring-shape: 1.75rem;
44 | }
45 |
--------------------------------------------------------------------------------
/apps/docs/src/hooks/useAutoTheme.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeColor,
3 | getCurrentMode,
4 | getCurrentSeedColor
5 | } from '../utils/theme'
6 |
7 | import React from 'react'
8 |
9 | const useAutoTheme = () => {
10 | React.useEffect(() => {
11 | if (typeof window.matchMedia !== 'function') return
12 |
13 | const isDark = window.matchMedia('(prefers-color-scheme: dark)')
14 | const isLight = window.matchMedia('(prefers-color-scheme: light)')
15 |
16 | if (typeof isLight.addEventListener === 'function') {
17 | const handleChange = () => {
18 | const mode = getCurrentMode()
19 | const color = getCurrentSeedColor()
20 | if (mode == 'system') {
21 | changeColor(color!)
22 | }
23 | }
24 | isDark.addEventListener('change', handleChange)
25 | isLight.addEventListener('change', handleChange)
26 | return () => {
27 | isDark.removeEventListener('change', handleChange)
28 | isLight.removeEventListener('change', handleChange)
29 | }
30 | }
31 | }, [])
32 | }
33 |
34 | export { useAutoTheme }
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Lerte Smith
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
13 | all 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
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/apps/docs/src/components/GettingStarted.tsx:
--------------------------------------------------------------------------------
1 | import { ListGroup, ListItem } from 'actify'
2 |
3 | import NavLink from './NavLink'
4 |
5 | const GettingStarted = () => {
6 | return (
7 |
8 |
11 | isActive ? 'block text-primary bg-surface-container-high' : ''
12 | }
13 | >
14 | Installation
15 |
16 |
19 | isActive ? 'block text-primary bg-surface-container-high' : ''
20 | }
21 | >
22 | Theme
23 |
24 |
27 | isActive ? 'block text-primary bg-surface-container-high' : ''
28 | }
29 | >
30 | Icon
31 |
32 |
33 | )
34 | }
35 |
36 | export default GettingStarted
37 |
--------------------------------------------------------------------------------
/apps/docs/content/components/focus-ring.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Focus Ring
3 | ---
4 |
5 | ## Usage
6 |
7 |
8 |
9 | ## Props
10 | | Name | Type | Description | Default |
11 | | --------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------- |
12 | | type | `outward` `inward` | The way the focus ring is animated and positioned. | `outward` |
13 | | as | `keyof HTMLElementTagNameMap` `React.ElementType` | Allows for more flexibility when supporting different elements that can act as a FocusRing. | `span` |
14 | | style | `React.CSSProperties` | — | — |
15 | | className | `string` | — | — |
--------------------------------------------------------------------------------
/packages/actify/src/components/Swiper/swiper.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | display: grid;
3 | width: 100%;
4 | overflow: hidden;
5 | place-items: center;
6 | }
7 | .root-inner {
8 | position: relative;
9 | height: 100%;
10 | display: flex;
11 | overflow: hidden;
12 | border-radius: 0.5rem;
13 | }
14 | .button {
15 | position: absolute;
16 | top: 50%;
17 | transform: traslateY(-50%);
18 | background-color: var(--md-sys-color-surface);
19 | }
20 | .button-prev {
21 | left: 1rem;
22 | }
23 | .button-next {
24 | right: 1rem;
25 | }
26 | .item {
27 | width: 100%;
28 | height: 100%;
29 | display: inline-block;
30 | flex: none;
31 | transition: var(--transition);
32 | transform: translateX(var(--transform));
33 | }
34 | .indicator {
35 | position: absolute;
36 | bottom: 1rem;
37 | display: flex;
38 | width: 100%;
39 | justify-content: center;
40 | gap: 0.5rem;
41 | }
42 | .indicator-item {
43 | width: 1.25rem;
44 | height: 1.25rem;
45 | border-radius: 9999px;
46 | cursor: pointer;
47 | background-color: var(--md-sys-color-inverse-surface);
48 | }
49 | .indicator-active {
50 | background-color: var(--md-sys-color-surface);
51 | }
52 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Cards/Card.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Elevation } from './../Elevation'
4 | import React from 'react'
5 | import { Ripple } from './../Ripple'
6 | import clsx from 'clsx'
7 | import styles from './card.module.css'
8 |
9 | interface CardProps extends React.ComponentProps<'div'> {
10 | ripple?: boolean
11 | elevation?: number
12 | variant?: 'elevated' | 'filled' | 'outlined'
13 | }
14 |
15 | const Card = (props: CardProps) => {
16 | const {
17 | id,
18 | ripple = false,
19 | variant = 'elevated',
20 | elevation = 1,
21 | children,
22 | className,
23 | ...rest
24 | } = props
25 |
26 | const cardId = id || `actify-card${React.useId()}`
27 |
28 | return (
29 |
30 |
{children}
31 | {ripple &&
}
32 | {variant === 'elevated' && (
33 |
34 | )}
35 |
36 | )
37 | }
38 |
39 | Card.displayName = 'Actify.Card'
40 |
41 | export { Card }
42 |
--------------------------------------------------------------------------------
/packages/actify/src/components/ActionMenu/MenuItems.tsx:
--------------------------------------------------------------------------------
1 | import { MenuItem } from './MenuItem'
2 | import type { Node } from '@react-types/shared'
3 | import React from 'react'
4 | import { TreeState } from 'react-stately'
5 | import styles from './menu.module.css'
6 | import { useMenuSection } from 'react-aria'
7 |
8 | interface MenuSectionProps {
9 | section: Node
10 | state: TreeState
11 | onAction: (key: React.Key) => void
12 | onClose: () => void
13 | }
14 |
15 | const MenuItems = ({
16 | section,
17 | state,
18 | onAction,
19 | onClose
20 | }: MenuSectionProps) => {
21 | const { itemProps, groupProps } = useMenuSection({
22 | heading: section.rendered,
23 | 'aria-label': section['aria-label']
24 | })
25 |
26 | return (
27 |
28 |
29 | {[...section.childNodes].map((node) => (
30 |
37 | ))}
38 |
39 |
40 | )
41 | }
42 |
43 | export { MenuItems }
44 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Field/OutlinedField.tsx:
--------------------------------------------------------------------------------
1 | import { Field, FieldProps } from './Field'
2 |
3 | import React from 'react'
4 | import styles from './styles/field.module.css'
5 |
6 | export interface OutlinedFieldProps
7 | extends FieldProps,
8 | React.ComponentProps<'div'> {}
9 |
10 | const renderOutline = (floatingLabel: React.JSX.Element | '') => (
11 |
12 |
13 |
14 |
15 |
16 |
{floatingLabel}
17 |
18 |
19 |
20 | )
21 |
22 | const OutlinedField = (props: OutlinedFieldProps) => {
23 | const { children, ...rest } = props
24 |
25 | return (
26 |
31 | {children}
32 |
33 | )
34 | }
35 |
36 | export { OutlinedField }
37 |
--------------------------------------------------------------------------------
/apps/docs/content/components/sliders.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Sliders
3 | description: Sliders let users make selections from a range of values
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | -------------- | ----------------------------------------- | -------------------- | ----------- |
14 | | `color` | `primary` `secondary` `tertiaray` `error` | color | `primary` |
15 | | `label` | `string` | label | `undefined` |
16 | | `value` | `number` `number[]` | slider value | `undefined` |
17 | | `labeled ` | `boolean` | show label on thumb | `undefined` |
18 | | `isDisabled` | `boolean` | disable slider | `undefined` |
19 | | `defaultValue` | `number` `number[]` | default slider value | `undefined` |
20 |
21 | ## Events
22 |
23 | | Events | Description |
24 | | ---------- | ------------------------ |
25 | | `onChange` | fired when value changed |
26 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Sponsors.tsx:
--------------------------------------------------------------------------------
1 | import { LinkButton } from 'actify'
2 | import NgrokerLogo from '@/components/NgrokerLogo'
3 | import TaildoorLogo from '@/components/TaildoorLogo'
4 |
5 | const Sponsors = () => {
6 | return (
7 |
28 | )
29 | }
30 |
31 | export default Sponsors
32 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Lists/list-group.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | position: relative;
3 | cursor: pointer;
4 | isolation: isolate;
5 | padding-left: 1rem /* 16px */;
6 | padding-right: 1rem /* 16px */;
7 | }
8 | .item {
9 | display: flex;
10 | align-items: center;
11 | height: 3.5rem /* 56px */;
12 | justify-content: space-between;
13 | }
14 | .icon {
15 | transition-property: transform;
16 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
17 | transition-duration: 150ms;
18 | }
19 | .icon-open {
20 | --tw-rotate: 90deg;
21 | transform: translate(var(--tw-translate-x), var(--tw-translate-y))
22 | rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
23 | scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
24 | }
25 | .hovered {
26 | position: absolute;
27 | inset: 0;
28 | z-index: -1;
29 | height: 3.5rem /* 56px */;
30 | background-color: var(--md-sys-color-surface-variant);
31 | }
32 | .content {
33 | display: grid;
34 | grid-template-rows: 0fr;
35 | transition-property: all;
36 | transition-duration: 300ms;
37 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
38 | }
39 | .content-open {
40 | grid-template-rows: 1fr;
41 | }
42 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/text-fields.tsx:
--------------------------------------------------------------------------------
1 | import { Icon, IconButton, TextField } from 'actify'
2 |
3 | import { useState } from 'react'
4 |
5 | export default () => {
6 | const [showPassword, setShowPassword] = useState(false)
7 | const handleClick = () => {
8 | setShowPassword((prev) => !prev)
9 | }
10 |
11 | return (
12 | <>
13 |
33 |
34 |
35 | >
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/swiper.tsx:
--------------------------------------------------------------------------------
1 | import { Swiper, SwiperItem } from 'actify'
2 |
3 | export default () => {
4 | const items = [
5 | {
6 | title: 'Swiper 1',
7 | src: 'https://images.unsplash.com/photo-1691977504044-fa2e8c813431?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=8'
8 | },
9 | {
10 | title: 'Swiper 2',
11 | src: 'https://images.unsplash.com/photo-1691763731792-c5ee77f9112a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1524&q=80'
12 | },
13 | {
14 | title: 'Swiper 3',
15 | src: 'https://images.unsplash.com/photo-1653916986137-996184bc4af0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1932&q=80'
16 | }
17 | ]
18 | return (
19 |
20 | {items.map((item, index) => (
21 |
22 |
27 |
28 | ))}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Pagination/generatePagination.ts:
--------------------------------------------------------------------------------
1 | const generatePagination = (currentPage: number, totalPages: number) => {
2 | // If the total number of pages is 7 or less,
3 | // display all pages without any ellipsis.
4 | if (totalPages <= 7) {
5 | return Array.from({ length: totalPages }, (_, i) => i + 1)
6 | }
7 |
8 | // If the current page is among the first 3 pages,
9 | // show the first 3, an ellipsis, and the last 2 pages.
10 | if (currentPage <= 3) {
11 | return [1, 2, 3, '...', totalPages - 1, totalPages]
12 | }
13 |
14 | // If the current page is among the last 3 pages,
15 | // show the first 2, an ellipsis, and the last 3 pages.
16 | if (currentPage >= totalPages - 2) {
17 | return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages]
18 | }
19 |
20 | // If the current page is somewhere in the middle,
21 | // show the first page, an ellipsis, the current page and its neighbors,
22 | // another ellipsis, and the last page.
23 | return [
24 | 1,
25 | '...',
26 | currentPage - 1,
27 | currentPage,
28 | currentPage + 1,
29 | '...',
30 | totalPages
31 | ]
32 | }
33 |
34 | export { generatePagination }
35 |
--------------------------------------------------------------------------------
/packages/actify/src/components/PopoverMenu/PopoverMenuItem.tsx:
--------------------------------------------------------------------------------
1 | import { Item, ItemProps } from './../Item'
2 |
3 | import { FocusRing } from './../FocusRing'
4 | import { PopoverMenuContext } from './PopoverMenu'
5 | import React from 'react'
6 | import { Ripple } from './../Ripple'
7 | import styles from './menu.module.css'
8 |
9 | interface PopoverMenuItemProps extends Omit {}
10 |
11 | export const PopoverMenuItem = (props: PopoverMenuItemProps) => {
12 | const { children, onClick, ...rest } = props
13 | const context = React.useContext(PopoverMenuContext)
14 |
15 | const handleClick = (event: React.MouseEvent) => {
16 | onClick?.(event)
17 | context?.setOpen(false)
18 | }
19 |
20 | const Container = () => (
21 |
22 |
23 |
24 |
25 | )
26 |
27 | return (
28 |
29 |
30 | }>
31 | {children}
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/packages/actify/src/hooks/useControllable.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useCallback, useEffect, useState } from 'react'
4 |
5 | type UseControllableProps = {
6 | value?: T
7 | onChange?: (value: T) => void
8 | defaultValue?: T
9 | }
10 |
11 | const useControllable = ({
12 | value,
13 | onChange,
14 | defaultValue
15 | }: UseControllableProps): [
16 | T,
17 | (event: React.ChangeEvent) => void
18 | ] => {
19 | const [internalValue, setInternalValue] = useState(defaultValue!)
20 |
21 | const isControlled = value !== undefined
22 |
23 | useEffect(() => {
24 | if (isControlled) {
25 | setInternalValue(value!)
26 | }
27 | }, [value, isControlled])
28 |
29 | const handleChange = useCallback(
30 | (event: React.ChangeEvent) => {
31 | const newValue = event.target.value as unknown as T
32 | if (isControlled) {
33 | onChange?.(newValue)
34 | } else {
35 | setInternalValue(newValue)
36 | }
37 | },
38 | [isControlled, onChange]
39 | )
40 |
41 | return [isControlled ? value! : internalValue, handleChange] as const
42 | }
43 |
44 | export { useControllable }
45 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Table/table.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | text-indent: 0;
3 | box-sizing: border-box;
4 | border-color: inherit;
5 | border: 0 solid #e5e7eb;
6 |
7 | width: 100%;
8 | position: relative;
9 | border-collapse: collapse;
10 | background-color: var(--md-sys-color-surface);
11 | box-shadow:
12 | rgba(0, 0, 0, 0.2) 0px 5px 5px -3px,
13 | rgba(0, 0, 0, 0.14) 0px 8px 10px 1px,
14 | rgba(0, 0, 0, 0.12) 0px 3px 14px 2px;
15 | }
16 | .th:focus-visible,
17 | .tr:focus-visible,
18 | .td:focus-visible {
19 | outline: none;
20 | }
21 |
22 | .th {
23 | position: relative;
24 | padding: 0 1rem;
25 | }
26 | .tr {
27 | border-color: inherit;
28 | border: 0 solid #e5e7eb;
29 |
30 | position: relative;
31 | height: 52px;
32 | border-style: solid;
33 | border-top-width: 1px;
34 | border-color: rgb(
35 | var(--md-sys-color-outline-variant) / var(--tw-border-opacity)
36 | );
37 | }
38 | .tr.hovered {
39 | background-color: var(--md-sys-color-inverse-surface) / 0.1;
40 | }
41 | .tr.selected {
42 | color: var(--md-sys-color-on-secondary-container);
43 | background-color: var(--md-sys-color-secondary-container);
44 | }
45 | .td {
46 | position: relative;
47 | padding: 0 1rem;
48 | }
49 |
--------------------------------------------------------------------------------
/apps/docs/content/components/carousel.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Carousel
3 | description: Carousels show a collection of items that can be scrolled on and off the screen
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | ---------- | ------------------ | ----------------------------------------------------------- | ------- |
14 | | autoPlay | `boolean` | Sets the autoplay mode for carousel | `false` |
15 | | control | `boolean` | Show prev and next button for carousel | `false` |
16 | | current | `number` | Sets carousel start play index | `0` |
17 | | indicator | `boolean` | Show the indicator for carousel | `false` |
18 | | infinite | `boolean` | Sets the infinite mode for carousel | `false` |
19 | | interval | `number` | Sets the interval duration for autoplay mode in miliseconds | `3000` |
20 | | `children` | `React.React.Node` | | `null` |
21 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Ripple/ripple.module.css:
--------------------------------------------------------------------------------
1 | .ripple {
2 | inset: 0;
3 | cursor: inherit;
4 | overflow: hidden;
5 | position: absolute;
6 | border-radius: inherit;
7 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
8 | &:before,
9 | &:after {
10 | content: '';
11 | opacity: 0;
12 | position: absolute;
13 | }
14 | &:before {
15 | background-color: var(
16 | --md-ripple-hover-color,
17 | var(--md-sys-color-on-surface)
18 | );
19 | inset: 0;
20 | transition:
21 | opacity 15ms linear,
22 | background-color 15ms linear;
23 | }
24 | &:after {
25 | background: radial-gradient(
26 | closest-side,
27 | var(--md-ripple-pressed-color, var(--md-sys-color-on-surface))
28 | max(100% - 70px, 65%),
29 | transparent 100%
30 | );
31 | transform-origin: center center;
32 | transition: opacity 375ms linear;
33 | }
34 | &.hovered:before {
35 | background-color: var(
36 | --md-ripple-hover-color,
37 | var(--md-sys-color-on-surface)
38 | );
39 | opacity: var(--md-ripple-hover-opacity, 0.08);
40 | }
41 | &.pressed:after {
42 | opacity: var(--md-ripple-pressed-opacity, 0.12);
43 | transition-duration: 105ms;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/apps/docs/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Icon } from 'actify'
2 |
3 | import NavLink from './NavLink'
4 | import React from 'react'
5 |
6 | interface DropdownProps extends React.ComponentProps<'div'> {
7 | items?: Array<{
8 | href: string
9 | text?: string
10 | }>
11 | }
12 | const Dropdown = (props: DropdownProps) => {
13 | const { title, items } = props
14 |
15 | return (
16 |
17 |
23 |
24 |
25 | {items?.map((item, index) => (
26 |
31 | {item.text}
32 |
33 | ))}
34 |
35 |
36 | )
37 | }
38 |
39 | export default Dropdown
40 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Accordion/Accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { AccordionProps, AccordionProvider } from './AccordionContext'
4 | import {
5 | Children,
6 | ComponentProps,
7 | cloneElement,
8 | isValidElement,
9 | useState
10 | } from 'react'
11 |
12 | import clsx from 'clsx'
13 | import styles from './accordion.module.css'
14 |
15 | interface AccordionRootProps extends AccordionProps, ComponentProps<'div'> {}
16 |
17 | const Accordion = (props: AccordionRootProps) => {
18 | const { multiple, children, className, open: openProp, ...rest } = props
19 | const [open, setOpen] = useState(openProp ?? [])
20 |
21 | return (
22 |
23 |
24 | {Children.map(
25 | children,
26 | (child, index) =>
27 | isValidElement(child) &&
28 | cloneElement(child, {
29 | index,
30 | // @ts-ignore
31 | ...child.props
32 | })
33 | )}
34 |
35 |
36 | )
37 | }
38 |
39 | Accordion.displayName = 'Acitfy.Accordion'
40 |
41 | export { Accordion }
42 |
--------------------------------------------------------------------------------
/apps/docs/src/lib/doc.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import matter from 'gray-matter'
3 | import path from 'path'
4 |
5 | const docsDirectory = path.join(process.cwd(), './content') // process.cwd() returns the absolute path of the current working directory
6 |
7 | function getFiles(dir: string, files: string[] = []) {
8 | const fileList = fs.readdirSync(dir)
9 | for (const file of fileList) {
10 | const name = `${dir}/${file}`
11 | if (fs.statSync(name).isDirectory()) {
12 | getFiles(name, files)
13 | } else {
14 | files.push(name)
15 | }
16 | }
17 | return files
18 | }
19 |
20 | export function getAllDocSlugs() {
21 | const files = getFiles(docsDirectory)
22 |
23 | return files.map((file) => {
24 | const relative = path.relative(docsDirectory, file)
25 | return { slug: relative.replace(/\.md?$/, '').split(path.sep) }
26 | })
27 | }
28 |
29 | export function getDocData(slugs: string[]) {
30 | const fullPath = path.join(docsDirectory, ...slugs) + '.md'
31 | const fileContents = fs.readFileSync(fullPath, 'utf8')
32 | const matterResult = matter(fileContents)
33 |
34 | return {
35 | content: matterResult.content,
36 | ...(matterResult.data as { title: string; description?: string })
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint"
9 | },
10 | "dependencies": {
11 | "@docsearch/react": "^3.9.0",
12 | "@material/material-color-utilities": "^0.3.0",
13 | "actify": "workspace:*",
14 | "codesandbox": "^2.2.3",
15 | "date-fns": "^3.6.0",
16 | "gray-matter": "^4.0.3",
17 | "next": "^15.1.0",
18 | "next-themes": "^0.4.3",
19 | "react": "catalog:",
20 | "react-dom": "catalog:",
21 | "react-markdown": "^9.0.1",
22 | "rehype-autolink-headings": "^7.1.0",
23 | "rehype-raw": "^7.0.0",
24 | "rehype-slug": "^6.0.0",
25 | "remark-gfm": "^4.0.0",
26 | "shiki": "^1.6.1",
27 | "sonner": "^1.7.0"
28 | },
29 | "devDependencies": {
30 | "@tailwindcss/postcss": "^4.1.4",
31 | "@tailwindcss/typography": "^0.5.16",
32 | "@types/node": "^20.12.12",
33 | "@types/react": "catalog:",
34 | "@types/react-dom": "catalog:",
35 | "clsx": "^2.1.1",
36 | "postcss": "^8.5.3",
37 | "react-live": "^4.1.6",
38 | "tailwind-merge": "^2.5.4",
39 | "tailwind-variants": "^0.2.1",
40 | "tailwindcss": "^4.1.4",
41 | "typescript": "^5.4.5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Snackbar/Toast.tsx:
--------------------------------------------------------------------------------
1 | import type { AriaToastProps } from '@react-aria/toast'
2 | import { Elevation } from '../Elevation'
3 | import { Icon } from '../Icon'
4 | import { IconButton } from '../Buttons'
5 | import React from 'react'
6 | import type { ToastState } from '@react-stately/toast'
7 | import styles from './toast.module.css'
8 | import { useToast } from '@react-aria/toast'
9 |
10 | interface ToastProps extends AriaToastProps {
11 | state: ToastState
12 | }
13 |
14 | const Toast = ({
15 | state,
16 | ...props
17 | }: ToastProps) => {
18 | const ref = React.useRef(null)
19 | const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
20 | props,
21 | state,
22 | ref
23 | )
24 |
25 | return (
26 |
27 |
28 |
29 | {props.toast.content}
30 |
31 |
32 |
33 | close
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export { Toast }
41 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/progress/circular-progress.tsx:
--------------------------------------------------------------------------------
1 | import { CircularProgress, Label, Slider, Switch } from 'actify'
2 |
3 | import { useState } from 'react'
4 |
5 | export default () => {
6 | const [value, setValue] = useState(50)
7 | const [isIndeterminate, setIsIndeterminate] = useState(true)
8 |
9 | const handleChange = (value: number | number[]) => {
10 | setValue(value as number)
11 | }
12 |
13 | return (
14 |
15 |
20 |
30 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import clsx from 'clsx'
3 | import styles from './icon.module.css'
4 |
5 | interface IconProps extends React.ComponentProps<'span'> {
6 | fill?: boolean
7 | }
8 | const Icon = ({ className, children, fill, ...rest }: IconProps) => {
9 | return (
10 |
13 |
20 |
21 | }
22 | >
23 |
28 |
36 | {children}
37 |
38 |
39 | )
40 | }
41 |
42 | Icon.displayName = 'Actify.Icon'
43 |
44 | export { Icon }
45 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/progress/linear-progress.tsx:
--------------------------------------------------------------------------------
1 | import { Label, LinearProgress, Slider, Switch } from 'actify'
2 |
3 | import { useState } from 'react'
4 |
5 | export default () => {
6 | const [value, setValue] = useState(50)
7 | const [isIndeterminate, setIsIndeterminate] = useState(true)
8 |
9 | const handleChange = (value: number | number[]) => {
10 | setValue(value as number)
11 | }
12 |
13 | return (
14 |
15 |
21 |
22 |
32 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Carousel/Carousel.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { CarouselContent } from './CarouselContent'
4 | import { CarouselControl } from './CarouselControl'
5 | import { CarouselIndicator } from './CarouselIndicator'
6 | import { CarouselProvider } from './CarouselContext'
7 | import React from 'react'
8 | import clsx from 'clsx'
9 | import styles from './carousel.module.css'
10 |
11 | interface CarouselProps extends React.ComponentProps<'div'> {
12 | autoPlay?: boolean
13 | control?: boolean
14 | infinite?: boolean
15 | indicator?: boolean
16 | children?: React.ReactNode[]
17 | }
18 |
19 | const Carousel = (props: CarouselProps) => {
20 | const {
21 | autoPlay,
22 | control,
23 | infinite,
24 | indicator,
25 | children,
26 | className,
27 | ...rest
28 | } = props
29 |
30 | return (
31 |
32 |
33 | {children}
34 |
35 | {indicator && }
36 |
37 |
38 | )
39 | }
40 |
41 | Carousel.displayName = 'Actify.Carousel'
42 |
43 | export { Carousel }
44 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Chips/ChipGroup.tsx:
--------------------------------------------------------------------------------
1 | import type { AriaTagGroupProps } from 'react-aria'
2 | import { Chip } from './Chip'
3 | import { Label } from '../Label'
4 | import React from 'react'
5 | import styles from './chip-group.module.css'
6 | import { useListState } from 'react-stately'
7 | import { useTagGroup } from 'react-aria'
8 |
9 | const ChipGroup = (props: AriaTagGroupProps) => {
10 | const { label, description, errorMessage } = props
11 | const ref = React.useRef(null)
12 |
13 | const state = useListState(props)
14 | const { gridProps, labelProps, descriptionProps, errorMessageProps } =
15 | useTagGroup(props, state, ref)
16 |
17 | return (
18 |
19 |
20 |
21 | {[...state.collection].map((item) => (
22 |
23 | ))}
24 |
25 | {description && (
26 |
27 | {description}
28 |
29 | )}
30 | {errorMessage && (
31 |
32 | {errorMessage}
33 |
34 | )}
35 |
36 | )
37 | }
38 |
39 | export { ChipGroup }
40 |
--------------------------------------------------------------------------------
/packages/actify/src/components/SideSheets/SideSheetsContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext } from 'react'
4 |
5 | import { useControllableState } from './../../hooks/useControllableState'
6 |
7 | interface SideSheetsProps {
8 | open?: boolean
9 | defaultOpen?: boolean
10 | divider?: boolean
11 | setOpen?: (open: boolean) => void
12 | }
13 |
14 | const SideSheetsContext = createContext(undefined)
15 |
16 | export interface SideSheetsProviderProps
17 | extends React.PropsWithChildren {}
18 |
19 | export const SideSheetsProvider = ({
20 | children,
21 | ...props
22 | }: SideSheetsProviderProps) => {
23 | const { open, defaultOpen, divider, setOpen } = props
24 | const [value, setValue] = useControllableState({
25 | value: open,
26 | defaultValue: defaultOpen,
27 | onChange: setOpen
28 | })
29 |
30 | return (
31 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export function useSideSheets() {
40 | const context = useContext(SideSheetsContext)
41 | if (!context) {
42 | throw new Error('SideSheets must be wrappered in the ')
43 | }
44 | return context
45 | }
46 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Select/OutlinedField.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | AriaButtonProps,
5 | mergeProps,
6 | useButton,
7 | useFocusRing,
8 | useHover
9 | } from 'react-aria'
10 |
11 | import React from 'react'
12 | import clsx from 'clsx'
13 | import styles from './outlined-field.module.css'
14 |
15 | type OutlinedFieldProps = React.ComponentProps<'button'> & AriaButtonProps
16 |
17 | const OutlinedField = (props: OutlinedFieldProps) => {
18 | const { ref, children } = props
19 |
20 | const buttonRef = React.useRef(null)
21 | const { buttonProps } = useButton(props, buttonRef)
22 | const { hoverProps, isHovered } = useHover(props)
23 | const { focusProps, isFocused } = useFocusRing()
24 |
25 | return (
26 |
27 | {/* outline */}
28 |
35 |
36 | {/* trigger button */}
37 |
44 |
45 | )
46 | }
47 |
48 | export { OutlinedField }
49 |
--------------------------------------------------------------------------------
/packages/actify/src/components/BottomSheets/BottomSheetsContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext } from 'react'
4 |
5 | import { useControllableState } from './../../hooks/useControllableState'
6 |
7 | interface BottomSheetsProps {
8 | open?: boolean
9 | defaultOpen?: boolean
10 | setOpen?: (open: boolean) => void
11 | }
12 |
13 | const BottomSheetsContext = createContext(
14 | undefined
15 | )
16 |
17 | export interface BottomSheetsProviderProps
18 | extends React.PropsWithChildren {}
19 |
20 | export const BottomSheetsProvider = ({
21 | children,
22 | ...props
23 | }: BottomSheetsProviderProps) => {
24 | const { open, defaultOpen, setOpen } = props
25 | const [value, setValue] = useControllableState({
26 | value: open,
27 | defaultValue: defaultOpen,
28 | onChange: setOpen
29 | })
30 |
31 | return (
32 |
33 | {children}
34 |
35 | )
36 | }
37 |
38 | export function useBottomSheets() {
39 | const context = useContext(BottomSheetsContext)
40 | if (!context) {
41 | throw new Error(
42 | 'BottomSheets components must be wrapped in '
43 | )
44 | }
45 | return context
46 | }
47 |
--------------------------------------------------------------------------------
/packages/actify/src/components/NavigationDrawer/DrawerContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext } from 'react'
4 |
5 | import { useControllableState } from './../../hooks/useControllableState'
6 |
7 | type Placement = 'left' | 'right' | 'top' | 'bottom'
8 |
9 | interface DrawerProps {
10 | open?: boolean
11 | placement?: Placement
12 | defaultOpen?: boolean
13 | setOpen?: (open: boolean) => void
14 | }
15 |
16 | const DrawerContext = createContext(undefined)
17 |
18 | export interface DrawerProviderProps
19 | extends React.PropsWithChildren {}
20 |
21 | export const DrawerProvider = ({ children, ...props }: DrawerProviderProps) => {
22 | const { open, placement, defaultOpen, setOpen } = props
23 |
24 | const [value, setValue] = useControllableState({
25 | value: open,
26 | defaultValue: defaultOpen,
27 | onChange: setOpen
28 | })
29 |
30 | return (
31 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export function useDrawer() {
40 | const context = useContext(DrawerContext)
41 | if (!context) {
42 | throw new Error('Drawer components must be wrapped in ')
43 | }
44 | return context
45 | }
46 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Tabs/tabs.module.css:
--------------------------------------------------------------------------------
1 | .tabs {
2 | padding: 8px;
3 | --_active-indicator-width: var(
4 | --md-secondary-tab-active-indicator-width,
5 | 2px
6 | );
7 | --_active-indicator-height: var(
8 | --md-secondary-tab-active-indicator-height,
9 | 2px
10 | );
11 | --_active-indicator-color: var(
12 | --md-secondary-tab-active-indicator-color,
13 | var(--md-sys-color-primary, #6750a4)
14 | );
15 | }
16 | .tabs-vertical {
17 | display: flex;
18 | }
19 | .tab {
20 | position: relative;
21 | }
22 | .tabpanel {
23 | flex: 1;
24 | }
25 | .tab-selection {
26 | position: absolute;
27 | will-change: transform, width;
28 | transition:
29 | transform 150ms,
30 | width 100ms;
31 | background: var(--_active-indicator-color);
32 | }
33 | .tab-selection-horizontal {
34 | left: 0;
35 | bottom: 2px;
36 | height: var(--_active-indicator-height);
37 | }
38 | .tab-selection-vertical {
39 | top: 0;
40 | right: 2px;
41 | width: var(--_active-indicator-width);
42 | }
43 | .tab-list {
44 | display: flex;
45 | gap: 8px;
46 | align-items: center;
47 | }
48 | .tab-list-vertical {
49 | flex-direction: column;
50 | }
51 | .tab-item {
52 | padding: 10px 12px;
53 | position: relative;
54 | display: flex;
55 | align-items: center;
56 | gap: 8px;
57 | &:focus-visible {
58 | outline: none;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/apps/docs/content/components/checkbox.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Checkbox
3 | description: Checkboxes let users select one or more items from a list, or turn an item on or off
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | ----------------- | ----------------------------------------- | ---------------------------------------- | ------- |
14 | | `color` | `primary` `secondary` `tertiaray` `error` | The color of the checkbox | `null` |
15 | | `value` | `boolean` | Checkbox value | `null` |
16 | | `isDisabled` | `boolean` | Whether the checkbox is disabled | `false` |
17 | | `isSelected` | `boolean` | whether the checkbox is selected | `false` |
18 | | `isIndeterminate` | `boolean` | whether the item is toggled on or off. | `false` |
19 | | `defaultSelected` | `boolean` | whether the checkbox is default selected | `false` |
20 |
21 | ## Events
22 |
23 | | Events | Description |
24 | | ---------- | ------------------------ |
25 | | `onChange` | fired when value changed |
26 |
--------------------------------------------------------------------------------
/apps/docs/src/app/(docs)/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getAllDocSlugs, getDocData } from '@/lib/doc'
2 |
3 | import Markdown from '@/components/Markdown'
4 | import TableOfContents from '@/components/TableOfContents'
5 | import { getFileRaw } from '@/lib/raw'
6 |
7 | type PageProps = {
8 | params: Promise<{ slug: string[] }>
9 | }
10 |
11 | export const generateMetadata = async (props: PageProps) => {
12 | const params = await props.params
13 | const { slug } = params
14 | const docs = getDocData(slug)
15 |
16 | return {
17 | title: docs?.title,
18 | description: docs?.description
19 | }
20 | }
21 |
22 | export async function generateStaticParams() {
23 | const slugs = getAllDocSlugs()
24 | return slugs
25 | }
26 |
27 | export default async function PageLayout(props: PageProps) {
28 | const params = await props.params
29 | const { slug } = params
30 | const docs = getDocData(slug)
31 |
32 | const filepath = slug.join('/').replace('components/', '')
33 | const content = await getFileRaw(`./src/usages/${filepath}.tsx`)
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Field/SupportingText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import supporting from './styles/supporting.module.css'
3 |
4 | export interface SupportingTextProps {
5 | max?: number
6 | count?: number
7 | error?: boolean
8 | errorText?: string
9 | supportingText?: string
10 | refreshErrorAlert?: boolean
11 | }
12 | const SupportingText = ({
13 | max = -1,
14 | count = -1,
15 | error,
16 | errorText,
17 | supportingText,
18 | refreshErrorAlert
19 | }: SupportingTextProps) => {
20 | const shouldErrorAnnounce = error && errorText && !refreshErrorAlert
21 | const role = shouldErrorAnnounce ? 'alert' : ''
22 |
23 | const counterText = () => {
24 | if (count < 0 || max <= 0) {
25 | return ''
26 | }
27 | return `${count} / ${max}`
28 | }
29 |
30 | const supportingOrErrorText = () => {
31 | return error && errorText ? errorText : supportingText
32 | }
33 |
34 | return (
35 |
36 |
37 | {supportingOrErrorText()}
38 | {counterText()}
39 |
40 |
41 | {`${supportingOrErrorText()} ${counterText()}`}
42 |
43 |
44 | )
45 | }
46 |
47 | export { SupportingText }
48 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/dialogs.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Dialog,
4 | DialogActivator,
5 | Modal,
6 | TextField,
7 | useDialogState
8 | } from 'actify'
9 |
10 | import React from 'react'
11 |
12 | export default () => {
13 | const state = useDialogState({})
14 | return (
15 |
16 |
17 | {(close) => (
18 |
25 | )}
26 |
27 |
30 |
31 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/menus.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Icon,
3 | IconButton,
4 | Menu,
5 | MenuButton,
6 | MenuItem,
7 | MenuPopover,
8 | Submenu
9 | } from 'actify'
10 |
11 | import React from 'react'
12 |
13 | export default () => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 | More_Horiz
25 |
26 | }
27 | >
28 |
29 |
30 |
31 |
32 |
33 |
34 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/apps/docs/src/usages/carousel.tsx:
--------------------------------------------------------------------------------
1 | import { Carousel, CarouselItem } from 'actify'
2 |
3 | export default () => {
4 | return (
5 |
6 |
7 |
12 |
13 |
14 |
19 |
20 |
21 |
26 |
27 | Custom Text
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/apps/docs/content/components/date-pickers.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Date pickers
3 | description: Date pickers let people select a date, or a range of dates
4 | ---
5 |
6 | ## Usage
7 |
8 |
9 |
10 | ## Props
11 |
12 | | Props | Type | Description | Default |
13 | | ---------------- | --------- | ---------------------------- | ------------ |
14 | | `asSingle` | `boolean` | render as single date picker | `false` |
15 | | `useRange` | `boolean` | use range mode | `true` |
16 | | `placeholder` | `string` | placeholder text | `null` |
17 | | `separator` | `string` | separator between range date | `~` |
18 | | `showShortcuts` | `boolean` | show left shortcuts date | `false` |
19 | | `showFooter` | `boolean` | show footer actions | `false` |
20 | | `displayFormat` | `string` | display format of date | `DD/MM/YYYY` |
21 | | `readOnly` | `boolean` | readonly mode | `false` |
22 | | `disabled` | `boolean` | disabled mode | `false` |
23 | | `inputClassName` | `string` | input class name | `null` |
24 | | `startFrom` | `Date` | start date for range mode | `new Date()` |
25 | | `minDate` | `Date` | min date | `null` |
26 | | `maxDate` | `Date` | max date | `null` |
27 |
--------------------------------------------------------------------------------
/packages/actify/src/components/ListBox/ListBoxOption.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AriaFocusRingProps,
3 | AriaOptionProps,
4 | mergeProps,
5 | useFocusRing,
6 | useOption
7 | } from 'react-aria'
8 | import { ListState, Node } from 'react-stately'
9 |
10 | import { FocusRing } from '../FocusRing'
11 | import React from 'react'
12 | import { Ripple } from './../Ripple'
13 | import clsx from 'clsx'
14 | import styles from './option.module.css'
15 |
16 | interface OptionProps extends AriaOptionProps, AriaFocusRingProps {
17 | item: Node
18 | state: ListState
19 | }
20 |
21 | const ListBoxOption = ({
22 | item,
23 | state,
24 | ...props
25 | }: OptionProps) => {
26 | const ref = React.useRef(null)
27 | const { focusProps, isFocusVisible } = useFocusRing(props)
28 | const { optionProps, isSelected } = useOption({ key: item.key }, state, ref)
29 |
30 | return (
31 |
36 | {item.rendered}
37 | {isFocusVisible && (
38 |
47 | )}
48 |
49 |
50 | )
51 | }
52 |
53 | export { ListBoxOption }
54 |
--------------------------------------------------------------------------------
/packages/create-actify/index.ts:
--------------------------------------------------------------------------------
1 | import { blue, yellow } from 'kolorist'
2 |
3 | import minimist from 'minimist'
4 | import path from 'node:path'
5 | import prompts from 'prompts'
6 |
7 | const argv = minimist<{
8 | t?: string
9 | template?: string
10 | }>(process.argv.slice(2), { string: ['_'] })
11 | const cwd = process.cwd()
12 |
13 | async function init() {
14 | const response = await prompts([
15 | {
16 | type: 'text',
17 | name: 'projectName',
18 | message: 'Project name:',
19 | initial: 'actify-project'
20 | },
21 | {
22 | type: 'select',
23 | name: 'framework',
24 | message: 'Select framework',
25 | choices: [
26 | {
27 | title: 'Vite',
28 | value: 'vite'
29 | },
30 | {
31 | title: 'Next.js',
32 | value: 'next'
33 | }
34 | ],
35 | initial: 0
36 | },
37 | {
38 | type: 'select',
39 | name: 'language',
40 | message: 'Select language',
41 | choices: [
42 | {
43 | title: blue('TypeScript'),
44 | value: 'typescript'
45 | },
46 | { title: yellow('JavaScript'), value: 'javascript' }
47 | ],
48 | initial: 0
49 | }
50 | ])
51 |
52 | console.log(response)
53 | }
54 |
55 | function formatTargetDir(targetDir: string | undefined) {
56 | return targetDir?.trim().replace(/\/+$/g, '')
57 | }
58 |
59 | init()
60 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Buttons/IconButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | Button as AriaButton,
5 | ButtonProps as AriaButtonProps
6 | } from 'react-aria-components'
7 | import React, { useId } from 'react'
8 | import { mergeProps, useButton, useFocusRing } from 'react-aria'
9 |
10 | import { FocusRing } from './../FocusRing'
11 | import { Ripple } from './../Ripple'
12 | import clsx from 'clsx'
13 | import styles from './icon-button.module.css'
14 |
15 | interface IconButtonProps extends AriaButtonProps {
16 | ref?: React.RefObject
17 | ripple?: boolean
18 | color?: 'primary' | 'secondary' | 'tertiary' | 'error'
19 | variant?: 'standard' | 'outlined' | 'filled' | 'filled-tonal'
20 | }
21 |
22 | const IconButton = (props: IconButtonProps) => {
23 | const { ref, ripple = true, children, className, isDisabled } = props
24 |
25 | const { focusProps, isFocusVisible } = useFocusRing()
26 |
27 | return (
28 |
37 | {/* FocusRing */}
38 | {isFocusVisible && }
39 | {/* Ripple */}
40 | {ripple && }
41 | <>{children}>
42 |
43 |
44 | )
45 | }
46 |
47 | IconButton.displayName = 'Actify.IconButton'
48 |
49 | export { IconButton }
50 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Checkbox/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import { AriaCheckboxGroupProps, useCheckboxGroup } from 'react-aria'
2 | import { CheckboxGroupState, useCheckboxGroupState } from 'react-stately'
3 |
4 | import { Label } from '../Label'
5 | import React from 'react'
6 | import { StyleProps } from '../../utils'
7 | import styles from './checkbox-group.module.css'
8 |
9 | export const CheckboxGroupContext =
10 | React.createContext(null)
11 |
12 | interface Props extends AriaCheckboxGroupProps, StyleProps {
13 | children?: React.ReactNode
14 | }
15 | const CheckboxGroup = (props: Props) => {
16 | const { children, label, description, style, className } = props
17 | const state = useCheckboxGroupState(props)
18 | const {
19 | groupProps,
20 | labelProps,
21 | descriptionProps,
22 | errorMessageProps,
23 | isInvalid,
24 | validationErrors
25 | } = useCheckboxGroup(props, state)
26 |
27 | return (
28 |
29 | {label &&
}
30 |
{children}
31 | {description && (
32 |
33 | {description}
34 |
35 | )}
36 | {isInvalid && (
37 |
38 | {validationErrors.join(' ')}
39 |
40 | )}
41 |
42 | )
43 | }
44 |
45 | export { CheckboxGroup }
46 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Tooltips/TooltipTrigger.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ReactNode, createContext, useRef } from 'react'
4 | import {
5 | TooltipTriggerProps,
6 | TooltipTriggerState,
7 | useTooltipTriggerState
8 | } from 'react-stately'
9 |
10 | import { FocusableElement } from '@react-types/shared'
11 | import { FocusableProvider } from '@react-aria/focus'
12 | import { Provider } from './../../utils'
13 | import { TooltipContext } from './Tooltip'
14 | import { useTooltipTrigger } from 'react-aria'
15 |
16 | export interface TooltipTriggerComponentProps extends TooltipTriggerProps {
17 | children: ReactNode
18 | }
19 |
20 | export const TooltipTriggerStateContext =
21 | createContext(null)
22 |
23 | /**
24 | * TooltipTrigger wraps around a trigger element and a Tooltip. It handles opening and closing
25 | * the Tooltip when the user hovers over or focuses the trigger, and positioning the Tooltip
26 | * relative to the trigger.
27 | */
28 | export function TooltipTrigger(props: TooltipTriggerComponentProps) {
29 | const state = useTooltipTriggerState(props)
30 | const ref = useRef(null)
31 | const { triggerProps, tooltipProps } = useTooltipTrigger(props, state, ref)
32 |
33 | return (
34 |
40 |
41 | {props.children}
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/packages/actify/src/components/ActionMenu/Menu.tsx:
--------------------------------------------------------------------------------
1 | import { AriaMenuProps, useMenu } from 'react-aria'
2 |
3 | import { MenuItem } from './MenuItem'
4 | import { MenuItems } from './MenuItems'
5 | import React from 'react'
6 | import clsx from 'clsx'
7 | import styles from './menu.module.css'
8 | import { useTreeState } from 'react-stately'
9 |
10 | interface MenuProps extends AriaMenuProps {
11 | onClose: () => void
12 | style?: React.CSSProperties
13 | className?: string
14 | }
15 |
16 | const Menu = (props: MenuProps) => {
17 | // Create state based on the incoming props
18 | const state = useTreeState(props)
19 |
20 | // Get props for the menu element
21 | const ref = React.useRef(null)
22 | const { menuProps } = useMenu(props, state, ref)
23 |
24 | return (
25 |
31 | {[...state.collection].map((item) =>
32 | item.type == 'section' ? (
33 | props.onAction}
39 | />
40 | ) : (
41 |
51 | )
52 | }
53 |
54 | export { Menu }
55 |
--------------------------------------------------------------------------------
/packages/actify/src/components/Accordion/AccordionContent.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useMemo } from 'react'
4 |
5 | import { Slot } from './../Slot'
6 | import { Text } from './../Text'
7 | import clsx from 'clsx'
8 | import styles from './accordion.module.css'
9 | import { useAccordion } from './AccordionContext'
10 |
11 | export interface AccordionContentProps
12 | extends Omit, 'children'> {
13 | index?: number
14 | asChild?: boolean
15 | children?:
16 | | React.ReactNode
17 | | ((props: { active?: boolean }) => React.ReactNode)
18 | }
19 |
20 | const AccordionContent = (props: AccordionContentProps) => {
21 | const { index, className, asChild, children, ...rest } = props
22 | const { open } = useAccordion()
23 |
24 | const active = useMemo(() => {
25 | if (open !== undefined) {
26 | return open[index as number]
27 | }
28 | }, [open, index])
29 |
30 | const classes = clsx(
31 | styles['accordion-content'],
32 | active && styles['active'],
33 | className
34 | )
35 |
36 | if (asChild) {
37 | return (
38 |
45 | {typeof children === 'function' ? children({ active }) : children}
46 |
47 | )
48 | }
49 |
50 | return (
51 |
52 | {children as React.ReactNode}
53 |
54 | )
55 | }
56 |
57 | export { AccordionContent }
58 |
--------------------------------------------------------------------------------