Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2021-09-27 15:48:04 +02:00
parent 9ab56544d9
commit b403a94a2e
16 changed files with 358 additions and 233 deletions

View File

@ -18,6 +18,19 @@ module.exports = {
}); });
newConfig.resolve.extensions.push('.ts', '.tsx'); newConfig.resolve.extensions.push('.ts', '.tsx');
// JavaScript
newConfig.module.rules.push({
test: /\.(js|jsx)$/,
// include: [path.resolve(__dirname, '../src/client/components')],
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}]
});
newConfig.resolve.extensions.push('.js', '.jsx');
// Stylus // Stylus
newConfig.module.rules.push({ newConfig.module.rules.push({
test: /\.styl$/, test: /\.styl$/,

11
package-lock.json generated
View File

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@dzeio/object-util": "^1.3.0",
"lucide-react": "^0.16.0", "lucide-react": "^0.16.0",
"rollup": "^2.44.0", "rollup": "^2.44.0",
"rollup-plugin-styles": "^3.14.1", "rollup-plugin-styles": "^3.14.1",
@ -1871,6 +1872,11 @@
"node": ">=0.1.95" "node": ">=0.1.95"
} }
}, },
"node_modules/@dzeio/object-util": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.3.0.tgz",
"integrity": "sha512-9pI7BdaS88NIM1RVP3pQ+oJoAPBtYJyInnIzFw3wE+j8W5eyTfudSQvkNoQW6SYrjxND7+IQRCMFwTor+lN5lA=="
},
"node_modules/@emotion/cache": { "node_modules/@emotion/cache": {
"version": "10.0.29", "version": "10.0.29",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz",
@ -24161,6 +24167,11 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@dzeio/object-util": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.3.0.tgz",
"integrity": "sha512-9pI7BdaS88NIM1RVP3pQ+oJoAPBtYJyInnIzFw3wE+j8W5eyTfudSQvkNoQW6SYrjxND7+IQRCMFwTor+lN5lA=="
},
"@emotion/cache": { "@emotion/cache": {
"version": "10.0.29", "version": "10.0.29",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz",

View File

@ -38,6 +38,7 @@
"postinstall": "rollup --config" "postinstall": "rollup --config"
}, },
"dependencies": { "dependencies": {
"@dzeio/object-util": "^1.3.0",
"lucide-react": "^0.16.0", "lucide-react": "^0.16.0",
"rollup": "^2.44.0", "rollup": "^2.44.0",
"rollup-plugin-styles": "^3.14.1", "rollup-plugin-styles": "^3.14.1",

View File

@ -1,5 +1,7 @@
.container .container
margin 48px auto padding 0 32px
padding 0 16px
width 100% width 100%
max-width 1312px max-width 1280px + @padding[1] * 2
margin auto auto
&.main
margin 48px auto

View File

@ -6,14 +6,11 @@ import css from './Container.module.styl'
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode
className?: string className?: string
mainContainer?: boolean
} }
export default class Container extends React.Component<Props> { export default class Container extends React.Component<Props> {
public render = () => ( public render = () => React.createElement(this.props.mainContainer ? 'main' : 'div', {className: buildClassName(css.container, this.props.className, [css.main, this.props.mainContainer]), children: this.props.children})
<main className={buildClassName(css.container, this.props.className)}>
{this.props.children}
</main>
)
} }

View File

@ -1,7 +1,7 @@
@import '../config.styl' @import '../config.styl'
.footer .footer
padding 16px padding 24px 0
background $foregroundLight background $foregroundLight
@media (prefers-color-scheme dark) @media (prefers-color-scheme dark)
background $foregroundDark background $foregroundDark
@ -9,14 +9,16 @@
ul ul
padding 0 padding 0
margin 0 margin 0
margin-top 12px
display flex display flex
justify-content center justify-content right
li li
display inline-block display inline-block
padding-left 24px
+ .icon
padding-left 16px
&:not(.icon) + .icon
padding-left 48px
&.socials a
padding 0 8px
.animation .animation
animation grow 1s linear infinite animation grow 1s linear infinite

View File

@ -5,6 +5,9 @@ import Text from '../Text'
import css from './Footer.module.styl' import css from './Footer.module.styl'
import Image from 'next/image' import Image from 'next/image'
import { Icon } from '../interfaces' import { Icon } from '../interfaces'
import Container from '../Container'
import Row from '../Row'
import Col from '../Col'
interface Props { interface Props {
text?: string text?: string
@ -22,27 +25,34 @@ interface Props {
export default class Footer extends React.Component<Props> { export default class Footer extends React.Component<Props> {
public render = () => ( public render = () => (
<footer className={css.footer}> <footer className={css.footer}>
<Container>
<Row nomargin>
<Col>
{this.props.text ? ( {this.props.text ? (
<Text align="center">{this.props.text}</Text> <Text>{this.props.text}</Text>
) : ( ) : (
<Text align="center">Made with <span className={css.animation}><Heart color={'#E6808A'} fill={'#E6808A'} size={16} fillOpacity={0.5} /></span> by {this.props.company || 'Dzeio'}</Text> <Text>Made with <span className={css.animation}><Heart color={'#E6808A'} fill={'#E6808A'} size={16} fillOpacity={0.5} /></span> by {this.props.company || 'Dzeio'}</Text>
)} )}
{this.props.links && ( </Col>
<ul>{this.props.links.map((l, index) => ( <Col>
<li key={l.path}><Text>{index !== 0 && (<>&nbsp;- </>)}<Link href={l.path}>{l.name}</Link></Text></li> <ul>
))}</ul> {this.props.links && this.props.links.map((l, index) => (
)} <li key={l.path + index}><Text><Link href={l.path} hideIcon>{l.name}</Link></Text></li>
{this.props.socials && ( ))}
<ul className={css.socials}>{this.props.socials.map((l, index) => ( {this.props.socials && this.props.socials.map((l, index) => (
<li key={l.href}><Text><Link hideIcon noStyle href={l.href}> <li key={l.href + index} className={css.icon}><Text><Link hideIcon noStyle href={l.href}>
{typeof l.icon === 'string' ? ( {typeof l.icon === 'string' ? (
<Image width={24} height={24} src={l.icon} /> <Image width={24} height={24} src={l.icon} />
) : ( ) : (
<l.icon size={24} /> <l.icon size={24} />
)} )}
</Link></Text></li> </Link></Text></li>
))}</ul> ))}
)} </ul>
</Col>
</Row>
</Container>
</footer> </footer>
) )
} }

View File

@ -15,6 +15,9 @@
color $darkGrayDark color $darkGrayDark
transition color $transition transition color $transition
pointer-events none pointer-events none
&.iconClickable
pointer-events all
cursor pointer
top 14px top 14px
&.left &.left
left 16px // input padding-left left 16px // input padding-left
@ -51,52 +54,25 @@
/* End */ /* End */
.autocomplete .autocomplete
display flex
opacity 0 opacity 0
transition all $transition transition all $transition
overflow-x hidden overflow-x hidden
pointer-events none pointer-events none
// display flex
flex-direction column
list-style none
position absolute position absolute
top calc(100% - 4px) top calc(100% + 16px)
left 0 left 0
width 100% width 100%
z-index 100 z-index 100
box-shadow 0 2px 4px rgba(black, .3)
margin 0
padding 0
background $foregroundLight
@media (prefers-color-scheme dark)
background $foregroundDark
border-radius 8px
max-height 25vh max-height 25vh
overflow-y auto overflow-y auto
@media (max-width $mobile) @media (max-width $mobile)
max-height 50vh max-height 50vh
&.reverse &.reverse
flex-direction column-reverse
top initial top initial
bottom 100% bottom calc(100% + 16px)
box-shadow 0 -4px 8px rgba(black, .3)
border-radius 0
border-top-left-radius 4px
border-top-right-radius 4px
li
transition all $transition
padding 8px
@media (max-width $mobile)
padding 24px
cursor pointer
&:hover
background darken(@background, 20%)
@media (prefers-color-scheme dark)
background lighten(lighten($foregroundDark, 5%), 20%)
div + .autocomplete div + .autocomplete
top calc(100% - 4px - .9em) top 100%
input:focus ~ .autocomplete input:focus ~ .autocomplete
select:focus ~ .autocomplete select:focus ~ .autocomplete
@ -127,9 +103,13 @@
font-size rem(16) font-size rem(16)
transition color $transition transition color $transition
opacity 1 opacity 1
color $darkGrayLight // color $darkGrayLight
// @media (prefers-color-scheme dark)
// color $darkGrayDark
color black
@media (prefers-color-scheme dark) @media (prefers-color-scheme dark)
color $darkGrayDark color white
&:disabled &:disabled
@ -162,14 +142,14 @@
~ svg ~ svg
color @border-color color @border-color
&::placeholder // &::placeholder
color black // color black
@media (prefers-color-scheme dark) // @media (prefers-color-scheme dark)
color white // color white
&:invalid &:invalid
border-color $errorDark border-color $errorLight
~ label ~ label
color @border-color color @border-color
@ -177,7 +157,7 @@
~ svg ~ svg
color @border-color color @border-color
@media (prefers-color-scheme dark) @media (prefers-color-scheme dark)
border-color $errorLight border-color $errorDark
~ label ~ label
color @border-color color @border-color
@ -190,53 +170,7 @@
padding-left 16px + 24px + 10px padding-left 16px + 24px + 10px
&.iconRight &.iconRight
padding-right 16 + 24 + 10px padding-right 16 + 24 + 10px
&.filled
border none
background rgba(gray, .1)
border-radius @border-radius @border-radius 0 0
border-bottom 2px solid rgba(black,.4)
@media (prefers-color-scheme dark)
background rgba(white, .1)
border-bottom 2px solid rgba(white,.4)
&.opaque
background white
@media (prefers-color-scheme dark)
background #202020
&:hover:not(:disabled)
background rgba(gray, .2)
@media (prefers-color-scheme dark)
background rgba(white, .2)
&.opaque
background darken(white, 5%)
@media (prefers-color-scheme dark)
background #1c1c1c
&:focus
background rgba(gray, .3)
border-bottom 2px solid $main
&.opaque
background white
@media (prefers-color-scheme dark)
background #202020
&:not(:placeholder-shown)
&:focus
&:not([placeholder=" "])
~ label
top 3px
left 16px - 4px // .input/padding-left label/padding-left
background transparent
padding 0
font-size .75rem
~ svg.left ~ label
left 16px + 24px + 10px // .input/padding-left label/padding-left
~ svg.rotate ~ svg.rotate
transform rotateX(0) transform rotateX(0)
transition $transition transition $transition
@ -249,6 +183,9 @@
padding 0 8px padding 0 8px
font-size rem(14) font-size rem(14)
&.block, &.block input, &.block select, &.block textarea &.block
&.block input
&.block select
&.block textarea
width 100% width 100%
display block display block

View File

@ -12,18 +12,26 @@ export default {
export const Basic: Story<any> = (args: any) => <Component {...args} /> export const Basic: Story<any> = (args: any) => <Component {...args} />
let tmp = Basic.bind({}) let tmp = Basic.bind({})
tmp.args = {label: 'Label', helper: 'Helper', maxLength: 6, characterCount: true, icon: X} tmp.args = {label: 'Label', helper: 'Helper', maxLength: 6, iconLeft: {
icon: X,
transformer: (v: string) => v + 1
}}
export const Normal = tmp export const Normal = tmp
tmp = Basic.bind({}) tmp = Basic.bind({})
tmp.args = {label: 'Label', filled:true, helper: 'Helper', autocomplete: ['a', 'b', 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'], characterCount: true, icon: X} tmp.args = {label: 'Label', filled:true, helper: 'Helper', choices: [
'a',
'a',
'a',
'a',
'a',
'a',
'a',
'a',
'b',
{value: 'd', display: 'D'},
'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
], characterCount: true, iconLeft: X}
export const AutoComplete = tmp export const AutoComplete = tmp
export const Select: Story<any> = (args: any) => <Component type="select" {...args}>
<option>a</option>
<option>b</option>
<option>c</option>
<option>d</option>
</Component>

View File

@ -5,22 +5,28 @@ import Text from '../Text'
import { Icon } from '../interfaces' import { Icon } from '../interfaces'
import { buildClassName } from '../Util' import { buildClassName } from '../Util'
import css from './Input.module.styl' import css from './Input.module.styl'
import Menu from '../Menu'
import { objectClone } from '@dzeio/object-util'
interface Props extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> { interface Props extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
id?: string id?: string
label?: string label?: string
icon?: Icon iconLeft?: Icon | {
iconRight?: Icon icon: Icon
transformer: (value: string) => string
}
iconRight?: Icon | {
icon: Icon
transformer: (value: string) => string
}
helper?: string helper?: string
inputRef?: React.RefObject<HTMLInputElement> inputRef?: React.RefObject<HTMLInputElement>
selectRef?: React.RefObject<HTMLSelectElement>
type?: 'color' | 'text' | 'date' | 'datetime-local' | type?: 'color' | 'text' | 'date' | 'datetime-local' |
'email' | 'file' | 'month' | 'number' | 'password' | 'email' | 'file' | 'month' | 'number' | 'password' |
'range' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'range' | 'search' | 'tel' | 'time' | 'url' | 'week' |
// Custom Types // Custom Types
'select' | 'textarea' 'textarea'
autocomplete?: Array<string> choices?: Array<string | {display: string, value: string}>
children?: React.ReactNode
} }
interface States { interface States {
@ -29,82 +35,57 @@ interface States {
isInFirstPartOfScreen?: boolean isInFirstPartOfScreen?: boolean
} }
export default class Input extends React.Component<Props, States> { export default class Input extends React.PureComponent<Props, States> {
public state: States = {} public state: States = {}
// any because f*ck types // any because f*ck types
private inputRef: React.RefObject<any> = React.createRef() private inputRef: React.RefObject<HTMLInputElement> = React.createRef()
private parentRef: React.RefObject<HTMLDivElement> = React.createRef() private parentRef: React.RefObject<HTMLDivElement> = React.createRef()
public componentDidMount() { public componentDidMount() {
if (this.props.type === 'textarea') { if (this.props.type === 'textarea') {
this.textareaHandler() this.textareaHandler()
} }
if (this.props.autocomplete) { if (this.props.choices) {
window.addEventListener('scroll', this.parentScroll) window.addEventListener('scroll', this.parentScroll)
this.parentScroll() this.parentScroll()
} }
} }
public componentDidUpdate() {
console.log(this.state)
}
public componentWillUnmount() { public componentWillUnmount() {
if (this.props.autocomplete) { if (this.props.choices) {
window.removeEventListener('scroll', this.parentScroll) window.removeEventListener('scroll', this.parentScroll)
} }
} }
public render() { public render() {
const props: Props = Object.assign({}, this.props) const props: Props = objectClone(this.props)
delete props.label delete props.label
delete props.children delete props.iconLeft
delete props.icon
delete props.helper
delete props.autocomplete
delete props.iconRight delete props.iconRight
delete props.inputRef delete props.inputRef
delete props.selectRef delete props.helper
delete props.color delete props.choices
const baseProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> = { const baseProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> = {
placeholder: this.props.label || this.props.placeholder || ' ', placeholder: this.props.label || this.props.placeholder || ' ',
ref: this.props.inputRef || this.inputRef, ref: this.props.inputRef || this.inputRef,
className: buildClassName( className: buildClassName(
[css.iconLeft, this.props.icon], [css.iconLeft, this.props.iconLeft],
[css.iconRight, this.props.iconRight || this.props.autocomplete] [css.iconRight, this.props.iconRight || this.props.choices]
), ),
onInvalid: (ev: React.FormEvent<HTMLInputElement>) => ev.preventDefault(), onInvalid: (ev: React.FormEvent<HTMLInputElement>) => ev.preventDefault(),
} }
let input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> let input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
if (this.props.type === 'number') { switch (this.props.type) {
baseProps.onWheel = (ev: React.WheelEvent<HTMLInputElement>) => ev.currentTarget.blur() case 'textarea':
}
if (this.props.type === 'select' && !this.props.readOnly) {
input = (
<select
ref={this.props.selectRef || this.inputRef}
{...props as React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>}
className={buildClassName(
[css.iconLeft, this.props.icon],
[css.iconRight, !this.props.disabled || this.props.iconRight],
[css[this.props.color as string], this.props.color]
)}
>
{this.props.children}
</select>
)
// select is readonly
} else if (this.props.type === 'select') {
input = (
<input
{...props}
{...baseProps}
type="text"
/>
)
} else if (this.props.type === 'textarea') {
delete baseProps.ref delete baseProps.ref
input = ( input = (
<textarea <textarea
@ -117,7 +98,10 @@ export default class Input extends React.Component<Props, States> {
onFocus={this.textareaHandler} onFocus={this.textareaHandler}
/> />
) )
} else { break
case 'number':
baseProps.onWheel = (ev: React.WheelEvent<HTMLInputElement>) => ev.currentTarget.blur()
default:
input = ( input = (
<input <input
{...props} {...props}
@ -134,17 +118,15 @@ export default class Input extends React.Component<Props, States> {
onChangeCapture={this.onChange} onChangeCapture={this.onChange}
ref={this.parentRef} ref={this.parentRef}
> >
{input} {input as any}
{/* Left Icon */} {/* Left Icon */}
{this.props.icon && ( {this.getIcon('left')}
<this.props.icon size="18" className={css.left} />
)}
{/* Right Icon */} {/* Right Icon */}
{this.props.iconRight ? ( {this.props.iconRight ?
<this.props.iconRight size="18" className={css.right} /> this.getIcon('right') :
) : ((this.props.type === 'select' || this.props.autocomplete) && !this.props.disabled) && ( (this.props.choices && !this.props.disabled) && (
<ChevronDown size="18" className={buildClassName(css.right, css.rotate)} /> <ChevronDown size="18" className={buildClassName(css.right, css.rotate)} />
)} )}
@ -154,10 +136,23 @@ export default class Input extends React.Component<Props, States> {
)} )}
{/* List when this is an autocomplete */} {/* List when this is an autocomplete */}
{this.props.autocomplete && this.props.autocomplete.indexOf(this.state?.value ?? this.props.value?.toString() ?? '') === -1 && ( {this.props.choices && (
<ul className={buildClassName(css.autocomplete, [css.reverse, !this.state.isInFirstPartOfScreen])}> // <ul className={buildClassName(css.autocomplete, [css.reverse, !this.state.isInFirstPartOfScreen])}>
{this.props.autocomplete.filter((item) => item.toLowerCase().includes(this.state?.value?.toLowerCase() ?? this.props.value?.toString().toLowerCase() ?? '')).map((item) => (<li key={item} onClick={this.onAutoCompleteClick(item)}><Text>{item}</Text></li>))} // {this.props.choices
</ul> // .map((item, index) => typeof item === 'string' ? ({item: {display: item, value: item}, index}) : {item, index})
// .filter(
// (item) => !this.getValue() || [item.item.display.toLowerCase(), item.item.value.toLowerCase()]
// .includes(this.getValue())
// )
// .map((item) => (<li key={item.index} onClick={this.onAutoCompleteClick(item.index)}><Text>{item.item.display}</Text></li>))}
// </ul>
<Menu
outline
hideWhenEmpty
className={buildClassName(css.autocomplete, [css.reverse, !this.state.isInFirstPartOfScreen])}
items={this.buildList()}
onClick={this.listSelection}
/>
)} )}
</div> </div>
) )
@ -176,8 +171,56 @@ export default class Input extends React.Component<Props, States> {
} }
} }
private buildList(): Menu['props']['items'] {
if (!this.props.choices) {
return []
}
const v = this.getValue().toLowerCase()
return this.props.choices
.map((item, index) => typeof item === 'string' ? ({item: {display: item, value: item}, index}) : {item, index})
.filter(
(item) => !v || item.item.display.toLowerCase().includes(v) || item.item.display.toLowerCase().toLowerCase().includes(v)
)
.map((item) => ({display: item.item.display, value: item.index}))
}
private listSelection: Menu['props']['onClick'] = async (value: number, key) => {
const newValue = this.props.choices?.[value]
if (!newValue) {
return
}
if (typeof newValue === 'string') {
return this.setValue(newValue)
}
await this.setValue(newValue.display)
this.setState({value: newValue.value})
}
private getIcon(icon: 'left' | 'right') {
const Icon = icon === 'left' ? this.props.iconLeft : this.props.iconRight
if (!Icon) {
return undefined
}
if ('icon' in Icon) {
return <Icon.icon size="18" className={buildClassName(css[icon], css.iconClickable)} onClick={() => {
const el = this.getElement()
console.log(el, 'pouet')
if (!el) {
return
}
el.value = Icon.transformer(el.value)
}} />
}
return <Icon size="18" className={css[icon]} />
}
private getValue(): string {
return this.state?.value?.toLowerCase() ?? this.props.value?.toString().toLowerCase() ?? ''
}
private getElement(): undefined | HTMLInputElement { private getElement(): undefined | HTMLInputElement {
const item = this.props.inputRef || this.props.selectRef || this.inputRef const item = this.props.inputRef || this.inputRef
if (!item || !item.current) {return} if (!item || !item.current) {return}
return item.current return item.current
} }
@ -188,8 +231,7 @@ export default class Input extends React.Component<Props, States> {
this.setState({textAreaHeight: this.inputRef.current.scrollHeight}) this.setState({textAreaHeight: this.inputRef.current.scrollHeight})
}) })
private onAutoCompleteClick = (value: string) => async () => { private async setValue(value: string) {
// console.log('test')
const item = this.getElement() const item = this.getElement()
if (!item) {return} if (!item) {return}
const valueSetter = Object.getOwnPropertyDescriptor(item, 'value')?.set const valueSetter = Object.getOwnPropertyDescriptor(item, 'value')?.set

View File

@ -0,0 +1,38 @@
@import "../config"
.menu
&.outline
border 2px solid $main
&.hidden
display none
ul
padding 0
margin 0
display flex
flex-direction column
li
svg
margin-right 8px
&:last-child
margin 0
display inline-flex
padding 8px
overflow-x hidden
text-overflow: ellipsis;
margin-bottom 8px
color $darkGrayLight
cursor pointer
font-weight bold
border-radius 8px
&:hover
color black
background $lightGrayLight
@media (prefers-color-scheme dark)
color $lightGrayDark
&:hover
color white
background $darkGrayDark
&.selected
&:active
background $main
color $textOnMain

View File

@ -0,0 +1,26 @@
import { Meta } from '@storybook/react/types-6-0'
import { XOctagon } from 'lucide-react'
import React from 'react'
import Component from '.'
export default {
title: 'DZEIO/Menu',
component: Component
} as Meta
const list: Component['props']['items'] = [
{value: 'Menu item 1'},
{value: 'Menu item 2', icon: XOctagon},
{value: 'Menu item 3', icon: XOctagon},
{value: 'Menu item 4', icon: XOctagon},
{value: 'Menu item 5', selected: true, icon: XOctagon},
{value: 'Menu item 6', icon: XOctagon},
{value: 'Menu item 7', icon: XOctagon},
{value: 'Menu item 8', icon: XOctagon},
{value: 'Menu item 9', icon: XOctagon},
{value: 'Menu item 10', icon: XOctagon}
]
export const Basic = (args: any) => (
<Component outline {...args} items={list} onClick={(_, index) => list[index].selected = !list[index].selected} />
)

31
src/dzeio/Menu/index.tsx Normal file
View File

@ -0,0 +1,31 @@
import React from 'react'
import Box from '../Box'
import { Icon } from '../interfaces'
import { buildClassName } from '../Util'
import css from './Menu.module.styl'
interface Props {
items: Array<{display?: string, value: any, selected?: boolean, icon?: Icon}>
outline?: boolean
onClick?: (value: any, key: number) => void
className?: string
hideWhenEmpty?: boolean
}
export default class Menu extends React.Component<Props> {
public render = () => (
<Box className={buildClassName(css.menu, this.props.className, [css.outline, this.props.outline], [css.hidden, this.props.hideWhenEmpty, this.props.items.length === 0])}>
<ul>
{this.props.items.map((item, key) => (
<li key={key} className={buildClassName([css.selected, item.selected])} onClick={() => this.props.onClick?.(item.value, key)}>
{item.icon && (
<item.icon size="24" />
)}
{item.display ?? item.value}
</li>
))}
</ul>
</Box>
)
}

View File

@ -1,4 +1,4 @@
export function buildClassName(...classes: Array<Array<any> | string | undefined>): string|undefined { export function buildClassName(...classes: Array<Array<any> | string | undefined>): string | undefined {
const classesFinal: Array<string> = [] const classesFinal: Array<string> = []
root: for (const classe of classes) { root: for (const classe of classes) {
if (typeof classe === 'undefined') { if (typeof classe === 'undefined') {

View File

@ -1,8 +1,15 @@
//@import "_aileron" //@import "_aileron"
@import "config" @import "config"
body *
*::before
*::after
box-sizing border-box box-sizing border-box
scrollbar-width 16px
scrollbar-color darken($foregroundLight, 16%) $foregroundLight
@media (prefers-color-scheme dark)
scrollbar-color lighten($foregroundDark, 16%) $foregroundDark
font-family: ui-system, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif font-family: ui-system, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif
// Georgia,Cambria,"Times New Roman",Times,serif // Georgia,Cambria,"Times New Roman",Times,serif
@ -25,9 +32,9 @@ a
text-decoration none text-decoration none
/* width */ /* width */
::-webkit-scrollbar { ::-webkit-scrollbar
width: 16px; width 16px
}
/* Track */ /* Track */
::-webkit-scrollbar-corner ::-webkit-scrollbar-corner

View File

@ -1,6 +1,4 @@
/** /**
* Copyright (c) 2021
*
* @summary DZEIO Component Library * @summary DZEIO Component Library
*/ */
@ -18,6 +16,7 @@ import Image from './dzeio/Image'
import Input from './dzeio/Input' import Input from './dzeio/Input'
import Link from './dzeio/Link' import Link from './dzeio/Link'
import Loader from './dzeio/Loader' import Loader from './dzeio/Loader'
import Menu from './dzeio/Menu'
import Navbar from './dzeio/Navbar' import Navbar from './dzeio/Navbar'
import NotificationManager from './dzeio/NotificationManager' import NotificationManager from './dzeio/NotificationManager'
import Popup from './dzeio/Popup' import Popup from './dzeio/Popup'
@ -39,6 +38,7 @@ export {
Input, Input,
Link, Link,
Loader, Loader,
Menu,
Navbar, Navbar,
NotificationManager, NotificationManager,
Popup, Popup,