Skip to content

Commit

Permalink
Add segment controls and tab views
Browse files Browse the repository at this point in the history
  • Loading branch information
1aron committed Jun 9, 2024
1 parent b739aaf commit 9f60ca9
Show file tree
Hide file tree
Showing 26 changed files with 335 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export { default } from './master.css'

export { default as button, buttonSizes, buttonColors } from './button'
export { default as select, selectSizes } from './select'
export { default as segments } from './segments'
export { default as segments, segmentsSizes } from './segments'
export { default as switch, switchSizes, switchColors } from './switch'
21 changes: 18 additions & 3 deletions packages/core/src/segments.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import type { Config } from '@master/css'

export const segmentsSizes = {
sm: 'p:1 r:4 h:24 {font:12;px:10;r:4}>.segment',
md: 'p:5 r:8 h:32 {font:12;px:10;r:4}>.segment',
lg: 'p:6 r:10 h:40 {font:14;px:12;r:5}>.segment'
}

export default {
styles: {
segments: 'flex bg:canvas p:5 r:2x w:fit',
segment: 'center-content flex font:12 font:medium leading:1.375rem px:3x r:1x white-space:nowrap {bg:surface;s:01;outline:1|line-lightest;fg:strong}.active'
segments: {
'': 'flex bg:canvas gap:2 w:fit',
...segmentsSizes
},
segment: {
'': 'center-content flex font:medium gap:6 white-space:nowrap {bg:surface;s:01;outline:1|line-lightest;fg:strong}.active',
icon: 'fg:lighter size:1em mx:-2'
}
}
} as Config
} as Config

// <div class="segment-tab">
// <div class="segment-tab">
12 changes: 12 additions & 0 deletions packages/react/src/Segment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ButtonHTMLAttributes } from 'react'
import clsx from 'clsx'

type SegmentProps = {
active?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement>

const Segment = ({ className, active, ...props }: SegmentProps) => {
return <button {...props} className={clsx('segment', className, active && 'active')} />
}

export default Segment
14 changes: 14 additions & 0 deletions packages/react/src/Segments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { HTMLAttributes } from 'react'
import type { segmentsSizes } from '@master/ui'
import clsx from 'clsx'

type SegmentsProps = {
// eslint-disable-next-line @typescript-eslint/ban-types
size?: (keyof typeof segmentsSizes) | (string & {})
} & HTMLAttributes<HTMLDivElement>

const Segments = ({ className, size = 'md', ...props }: SegmentsProps) => {
return <div {...props} className={clsx('segments', size && `segments-${size}`, className)} />
}

export default Segments
21 changes: 21 additions & 0 deletions packages/react/src/TabControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import { type ElementType, type ComponentPropsWithoutRef, useMemo } from 'react'
import clsx from 'clsx'
import { useTabView } from './TabView'

type TabControlProps<T extends ElementType> = {
name: string
as?: T
disabled?: boolean
} & ComponentPropsWithoutRef<T>

export default function TabControl<T extends ElementType = 'button'>({ as, className, disabled, ...props }: TabControlProps<T>) {
const tabView = useTabView()
const Component = as || 'button'
return <Component {...props}
className={clsx(!Component && 'tab', className, tabView.activeTab === props.name && 'active')}
onClick={() => tabView.setActiveTab(props.name)}
disabled={Component === 'button' ? disabled : undefined}
/>
}
17 changes: 17 additions & 0 deletions packages/react/src/TabPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import type { ElementType, ComponentPropsWithoutRef } from 'react'
import { useTabView } from './TabView'

type TabPaneProps<T extends ElementType> = {
name: string
as?: T
} & ComponentPropsWithoutRef<T>

const TabPane = <T extends ElementType = 'div'>({ as, ...props }: TabPaneProps<T>) => {
const tabView = useTabView()
const Component = as || 'div'
return <Component {...props} hidden={tabView.activeTab !== props.name} />
}

export default TabPane
34 changes: 34 additions & 0 deletions packages/react/src/TabView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'

import { FC, ReactNode, createContext, useContext, useState } from 'react'

type TabViewContextType = {
activeTab?: string
setActiveTab: (tab: string) => void
};

export const TabViewContext = createContext<TabViewContextType | undefined>(undefined)

export const useTabView = () => {
const context = useContext(TabViewContext)
if (context === undefined) {
throw new Error('useTabView must be used within a TabView')
}
return context
}

type TabViewProps = {
activeTab?: string
children: ReactNode
}

const TabView: FC<TabViewProps> = (props) => {
const [activeTab, setActiveTab] = useState<string | undefined>(props.activeTab)
return (
<TabViewContext.Provider value={{ activeTab, setActiveTab }}>
{props.children}
</TabViewContext.Provider>
)
}

export default TabView
7 changes: 7 additions & 0 deletions packages/react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export { default as Switch } from './Switch'
export { default as Select } from './Select'
export { default as SelectIndicator } from './SelectIndicator'
export { default as SelectChevronIndicator } from './SelectChevronIndicator'
export { default as Segments } from './Segments'
export { default as Segment } from './Segment'
export { default as TabView } from './TabView'
export { default as TabControl } from './TabControl'
export { default as TabPane } from './TabPane'


2 changes: 1 addition & 1 deletion site/app/[locale]/components/button/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import define from 'internal/utils/metadata'
const metadata = define({
title: 'Button',
description: 'Beautiful button components crafted with care in every size, color and shape.',
category: 'Element',
category: 'Control',
openGraph: {
images: new URL('~/site/public/images/components/button.jpg', import.meta.url).toString()
},
Expand Down
2 changes: 1 addition & 1 deletion site/app/[locale]/components/checkbox/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import define from 'internal/utils/metadata'
const metadata = define({
title: 'Checkbox',
description: 'Beautiful checkbox components crafted with care in every size, color and interaction.',
category: 'Element',
category: 'Control',
openGraph: {
images: new URL('~/site/public/images/components/checkbox.jpg', import.meta.url).toString()
},
Expand Down
3 changes: 2 additions & 1 deletion site/app/[locale]/components/components/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default function Overview() {
.filter(({ name }) => name !== 'Overview')
.map((category, index) => (
<Fragment key={index}>
<h2>{category.name}</h2>
<hr />
<h3 id={category.name}>{category.name}</h3>
<div className="grid-cols:2 grid-cols:4@sm mt:5x gap:5x">
{
category.definedMetadataList.map((definedMetadata, index) => (
Expand Down
9 changes: 9 additions & 0 deletions site/app/[locale]/components/segments/content.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Normal
<CodePreview src="segments/normal"
raw={require('!!raw-loader!../../previews/segments/normal/content').default}
children={require('../../previews/segments/normal/content').default()} />

## With leading icon
<CodePreview src="segments/with-leading-icon"
raw={require('!!raw-loader!../../previews/segments/with-leading-icon/content').default}
children={require('../../previews/segments/with-leading-icon/content').default()} />
13 changes: 13 additions & 0 deletions site/app/[locale]/components/segments/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import define from 'internal/utils/metadata'

const metadata = define({
title: 'Segments',
description: 'A set of segment controls, each of which functions as a button.',
category: 'Control',
openGraph: {
images: new URL('~/site/public/images/components/select.jpg', import.meta.url).toString()
},
filename: import.meta.url
})

export default metadata
21 changes: 21 additions & 0 deletions site/app/[locale]/components/segments/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Layout from 'internal/layouts/doc'
import metadata from './metadata'
/* @ts-expect-error toc */
import Content, { toc } from './content.mdx'
import generate from 'internal/utils/generate-metadata'
import { getUnitCategories } from '~/site/metadata'

export const dynamic = 'force-static'
export const revalidate = false

export async function generateMetadata(props: any, parent: any) {
return await generate(metadata, props, parent)
}

export default async function Page(props: any) {
return (
<Layout {...props} $type="preview" pageCategories={getUnitCategories('components')} pageDirname={__dirname} metadata={metadata} toc={toc} >
<Content />
</Layout >
)
}
2 changes: 1 addition & 1 deletion site/app/[locale]/components/select/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import define from 'internal/utils/metadata'
const metadata = define({
title: 'Select',
description: 'Beautiful select components crafted with care in every size, color and interaction.',
category: 'Element',
category: 'Control',
openGraph: {
images: new URL('~/site/public/images/components/select.jpg', import.meta.url).toString()
},
Expand Down
2 changes: 1 addition & 1 deletion site/app/[locale]/components/switch/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import define from 'internal/utils/metadata'
const metadata = define({
title: 'Switch',
description: 'Beautiful toggle switch components crafted with care in every size, color and interaction.',
category: 'Element',
category: 'Control',
openGraph: {
images: new URL('~/site/public/images/components/switch.jpg', import.meta.url).toString()
},
Expand Down
6 changes: 6 additions & 0 deletions site/app/[locale]/components/tab-view/content.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Basic from '../../previews/tab-view/basic/content'

## Basic
<CodePreview src="tab-view/basic" raw={require('!!raw-loader!../../previews/tab-view/basic/content').default}>
<Basic />
</CodePreview>
13 changes: 13 additions & 0 deletions site/app/[locale]/components/tab-view/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import define from 'internal/utils/metadata'

const metadata = define({
title: 'Tab View',
description: 'A tab view presents multiple mutually exclusive content panes in the same context area, which users can switch between.',
category: 'View Organizing',
openGraph: {
images: new URL('~/site/public/images/components/select.jpg', import.meta.url).toString()
},
filename: import.meta.url
})

export default metadata
21 changes: 21 additions & 0 deletions site/app/[locale]/components/tab-view/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Layout from 'internal/layouts/doc'
import metadata from './metadata'
/* @ts-expect-error toc */
import Content, { toc } from './content.mdx'
import generate from 'internal/utils/generate-metadata'
import { getUnitCategories } from '~/site/metadata'

export const dynamic = 'force-static'
export const revalidate = false

export async function generateMetadata(props: any, parent: any) {
return await generate(metadata, props, parent)
}

export default async function Page(props: any) {
return (
<Layout {...props} $type="preview" pageCategories={getUnitCategories('components')} pageDirname={__dirname} metadata={metadata} toc={toc} >
<Content />
</Layout >
)
}
20 changes: 19 additions & 1 deletion site/app/[locale]/previews/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client'

import { useLayoutEffect } from 'react'
import { useEffect, useLayoutEffect } from 'react'
import type ThemeMode from 'theme-mode'
import { useThemeMode } from '@master/theme-mode.react'

export default function Layout({ children }: {
children: JSX.Element
}) {
const themeMode = useThemeMode()
useLayoutEffect(() => {
const sendHeight = () => {
const height = document.body.scrollHeight
Expand All @@ -16,5 +19,20 @@ export default function Layout({ children }: {
resizeObserver.disconnect()
}
})

useEffect(() => {
if (window.parent) {
const handleThemeModeChange = (e: any) => {
const parentThemeMode = e.detail as ThemeMode
if (parentThemeMode.value) {
themeMode.value = parentThemeMode.value
}
}
window.parent.document.documentElement.addEventListener('themeModeChange', handleThemeModeChange)
return () => {
window.parent.document.documentElement.removeEventListener('themeModeChange', handleThemeModeChange)
}
}
}, [themeMode])
return children
}
16 changes: 16 additions & 0 deletions site/app/[locale]/previews/segments/normal/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Segment, Segments } from '@master/ui.react'

export default () => <>
<Segments size='sm'>
<Segment active>Grid</Segment>
<Segment>List</Segment>
</Segments>
<Segments>
<Segment active>Grid</Segment>
<Segment>List</Segment>
</Segments>
<Segments size='lg'>
<Segment active>Grid</Segment>
<Segment>List</Segment>
</Segments>
</>
9 changes: 9 additions & 0 deletions site/app/[locale]/previews/segments/normal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Content from './content'

export default function Page() {
return (
<div className="app-demo gap:8x flex:col@<2xs bg:none!">
<Content />
</div>
)
}
35 changes: 35 additions & 0 deletions site/app/[locale]/previews/segments/with-leading-icon/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Segment, Segments } from '@master/ui.react'
import { IconLayoutGrid, IconList } from '@tabler/icons-react'

export default () => <>
<Segments size="sm">
<Segment active>
<IconLayoutGrid className='segment-icon' />
Grid
</Segment>
<Segment>
<IconList className='segment-icon' />
List
</Segment>
</Segments>
<Segments>
<Segment active>
<IconLayoutGrid className='segment-icon' />
Grid
</Segment>
<Segment>
<IconList className='segment-icon' />
List
</Segment>
</Segments>
<Segments size="lg">
<Segment active>
<IconLayoutGrid className='segment-icon' />
Grid
</Segment>
<Segment>
<IconList className='segment-icon' />
List
</Segment>
</Segments>
</>
Loading

0 comments on commit 9f60ca9

Please sign in to comment.