From 179287bf06a5693b55eb6fe8935feff0166c81ed Mon Sep 17 00:00:00 2001
From: Vass Bence <49574140+vassbence@users.noreply.github.com>
Date: Wed, 9 Oct 2024 17:56:24 +0200
Subject: [PATCH] work
---
src/components/Form/Form.stories.tsx | 149 +++++++-
src/components/Form/Form.tsx | 406 ++++++++++++----------
src/components/Form/FormField.stories.tsx | 23 ++
src/components/Form/FormField.tsx | 5 +
src/components/Form/FormFieldGroup.tsx | 27 +-
src/components/Form/FormSidebar.tsx | 42 +++
src/components/Form/index.tsx | 1 +
src/components/Note/Note.stories.tsx | 52 +++
src/components/Note/index.tsx | 112 ++++++
src/components/Patterns.stories.tsx | 3 -
src/components/Sidebar/index.tsx | 19 +-
src/index.ts | 1 +
12 files changed, 632 insertions(+), 208 deletions(-)
create mode 100644 src/components/Form/FormSidebar.tsx
create mode 100644 src/components/Note/Note.stories.tsx
create mode 100644 src/components/Note/index.tsx
diff --git a/src/components/Form/Form.stories.tsx b/src/components/Form/Form.stories.tsx
index 337cc04..90af798 100644
--- a/src/components/Form/Form.stories.tsx
+++ b/src/components/Form/Form.stories.tsx
@@ -1,19 +1,19 @@
import React, { useEffect, useState } from 'react'
-import { FormErrors, FormField, FormFieldGroup, useForm } from './index.js'
-import { TextInput } from '../TextInput/index.js'
-import { TextAreaInput } from '../TextAreaInput/index.js'
-import { NumberInput } from '../NumberInput/index.js'
-import { CheckboxInput } from '../CheckboxInput/index.js'
-import { SwitchInput } from '../SwitchInput/index.js'
-import { Calendar } from '../Calendar/index.js'
+import { FormErrors } from './index.js'
import { Button } from '../Button/index.js'
-import { format } from 'date-fns'
import { Form } from './Form.js'
import { Badge } from '../Badge/index.js'
+import { Sidebar } from '../Sidebar/index.js'
+import { AppHeader } from '../AppHeader/index.js'
+import { colors } from '../../utils/colors.js'
+import { ScrollArea } from '../ScrollArea/index.js'
export default {
title: 'Form',
component: () => {},
+ parameters: {
+ layout: 'fullscreen',
+ },
}
export const Component = () => {
@@ -126,6 +126,139 @@ export const Component = () => {
)
}
+export const FullScreenGroups = () => {
+ return (
+
+ )
+}
+
export const Async = () => {
const [data, setData] = useState()
diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx
index 96f61d0..e5601a2 100644
--- a/src/components/Form/Form.tsx
+++ b/src/components/Form/Form.tsx
@@ -1,29 +1,25 @@
import { createContext, ReactNode, useContext, useMemo } from 'react'
import { useForm, UseFormProps } from './useForm.js'
-import { FormField } from './FormField.js'
+import { FormField, FormFieldProps } from './FormField.js'
import { FormFieldGroup } from './FormFieldGroup.js'
import { TextInput } from '../TextInput/index.js'
import { TextAreaInput } from '../TextAreaInput/index.js'
import { SwitchInput } from '../SwitchInput/index.js'
import { CheckboxInput } from '../CheckboxInput/index.js'
import { NumberInput } from '../NumberInput/index.js'
-import { Calendar } from '../Calendar/index.js'
-import { Button } from '../Button/index.js'
-import { format } from 'date-fns'
import { SelectInput, SelectInputProps } from '../SelectInput/index.js'
import { DateInput, DateInputProps } from '../DateInput/index.js'
import { RichTextEditor } from '../RichTextEditor/index.js'
+import { IconProps } from '../Icon/index.js'
+import { FormValues } from './types.js'
+import { FormSidebar } from './FormSidebar.js'
const FormContext = createContext<{
- fields: FormProps['fields']
+ fields: FormFields
+ groups: FormProps['groups']
form: ReturnType
} | null>(null)
-type CommonFormFieldProps = {
- label?: string
- description?: string
-}
-
type TextFormField = { type: 'text' }
type TextAreaFormField = { type: 'textarea' }
type NumberFormField = { type: 'number' }
@@ -34,29 +30,42 @@ type SelectFormField = {
type: 'select'
} & Pick
type RichTextFormField = { type: 'richtext' }
+type FormFields = {
+ [key: string]: (
+ | TextFormField
+ | TextAreaFormField
+ | NumberFormField
+ | SwitchFormField
+ | CheckboxFormField
+ | DateTimeFormField
+ | SelectFormField
+ | RichTextFormField
+ ) &
+ Pick
+}
type FormProps = {
children: ReactNode | ((form: ReturnType) => React.ReactNode)
- fields: {
- [key: string]: (
- | TextFormField
- | TextAreaFormField
- | NumberFormField
- | SwitchFormField
- | CheckboxFormField
- | DateTimeFormField
- | SelectFormField
- | RichTextFormField
- ) &
- CommonFormFieldProps
- }
+ groups?: {
+ label: string
+ description?: string
+ icon?: IconProps['variant']
+ fields: string[]
+ }[]
+ fields: FormFields | ((values: FormValues) => FormFields)
} & UseFormProps
-function Form({ fields, children, ...useFormProps }: FormProps) {
+function Form({ fields, groups, children, ...useFormProps }: FormProps) {
const form = useForm(useFormProps)
return (
-
+
{typeof children === 'function' ? children(form) : children}
)
@@ -64,176 +73,209 @@ function Form({ fields, children, ...useFormProps }: FormProps) {
type FormFieldsProps = {
horizontal?: boolean
+ fullScreen?: boolean
}
-function FormFields({ horizontal }: FormFieldsProps) {
+// TODO move into it's own component
+function FormFields({ horizontal, fullScreen }: FormFieldsProps) {
const ctx = useContext(FormContext)
if (!ctx) {
throw new Error('missing form context')
}
- const { fields, form } = ctx
+ const { fields, groups, form } = ctx
// TODO do not repeat the FormFields so much, make them only once
- const children = useMemo(
- () =>
- Object.entries(fields).map(([key, field]) => {
- switch (field.type) {
- case 'text':
- return (
-
- form.setValue(key, value)}
- error={!!form.errors[key]}
- />
-
- )
- case 'textarea':
- return (
-
- form.setValue(key, value)}
- error={!!form.errors[key]}
- />
-
- )
- case 'switch':
- return (
-
- form.setValue(key, value)}
- error={!!form.errors[key]}
- />
-
- )
- case 'checkbox':
- return (
-
- form.setValue(key, value)}
- error={!!form.errors[key]}
- />
-
- )
- case 'number':
- return (
-
- form.setValue(key, value)}
- error={!!form.errors[key]}
- />
-
- )
- case 'datetime':
- return (
-
- {
- form.setValue(key, value)
- }}
- />
-
- )
- case 'select':
- return (
-
- {
- form.setValue(key, value)
- }}
- filterable={field.filterable}
- options={field.options}
- />
-
- )
- case 'richtext':
- return (
-
- {/* TODO missing disabled and error */}
- {
- form.setValue(key, value)
- }}
- />
-
- )
- default:
- throw new Error('Form: unknown field type')
- }
- }),
- [fields, form],
- )
+ function renderFields(fields: FormProps['fields']) {
+ return Object.entries(fields).map(([key, field]) => {
+ switch (field.type) {
+ case 'text':
+ return (
+
+ form.setValue(key, value)}
+ error={!!form.errors[key]}
+ />
+
+ )
+ case 'textarea':
+ return (
+
+ form.setValue(key, value)}
+ error={!!form.errors[key]}
+ />
+
+ )
+ case 'switch':
+ return (
+
+ form.setValue(key, value)}
+ error={!!form.errors[key]}
+ />
+
+ )
+ case 'checkbox':
+ return (
+
+ form.setValue(key, value)}
+ error={!!form.errors[key]}
+ />
+
+ )
+ case 'number':
+ return (
+
+ form.setValue(key, value)}
+ error={!!form.errors[key]}
+ />
+
+ )
+ case 'datetime':
+ return (
+
+ {
+ form.setValue(key, value)
+ }}
+ />
+
+ )
+ case 'select':
+ return (
+
+ {
+ form.setValue(key, value)
+ }}
+ filterable={field.filterable}
+ options={field.options}
+ />
+
+ )
+ case 'richtext':
+ return (
+
+ {/* TODO missing disabled and error */}
+ {
+ form.setValue(key, value)
+ }}
+ />
+
+ )
+ default:
+ throw new Error('Form: unknown field type')
+ }
+ })
+ }
- return {children}
+ if (groups) {
+ return groups.map((group) => (
+
+ {renderFields(
+ Object.keys(fields).reduce((acc, curr) => {
+ if (group.fields.includes(curr)) {
+ acc[curr] = fields[curr]
+ }
+
+ return acc
+ }, {} as FormFields),
+ )}
+
+ ))
+ }
+
+ return (
+
+ {renderFields(fields)}
+
+ )
}
Form.Fields = FormFields
+Form.Sidebar = FormSidebar
-export { Form }
+export { Form, FormContext }
export type { FormProps, FormFieldsProps }
diff --git a/src/components/Form/FormField.stories.tsx b/src/components/Form/FormField.stories.tsx
index e4ada27..2d3091f 100644
--- a/src/components/Form/FormField.stories.tsx
+++ b/src/components/Form/FormField.stories.tsx
@@ -15,6 +15,17 @@ export const Vertical = () => {
)
}
+export const VerticalNote = () => {
+ return (
+
+ {}} />
+
+ )
+}
+
export const Horizontal = () => {
return (
{
)
}
+
+export const HorizontalNote = () => {
+ return (
+
+ {}} />
+
+ )
+}
diff --git a/src/components/Form/FormField.tsx b/src/components/Form/FormField.tsx
index 311ea6e..6571ad6 100644
--- a/src/components/Form/FormField.tsx
+++ b/src/components/Form/FormField.tsx
@@ -2,6 +2,7 @@ import { ReactNode } from 'react'
import { Text } from '../Text/index.js'
import { colors } from '../../utils/colors.js'
import { Icon } from '../Icon/index.js'
+import { Note, NoteProps } from '../Note/index.js'
type FormFieldProps = {
label?: string
@@ -9,6 +10,7 @@ type FormFieldProps = {
error?: string
children: ReactNode
horizontal?: boolean
+ note?: Pick
}
function FormField({
@@ -17,6 +19,7 @@ function FormField({
description,
error,
horizontal = false,
+ note,
}: FormFieldProps) {
if (horizontal) {
return (
@@ -65,6 +68,7 @@ function FormField({
{description}
)}
+ {note && }
)
@@ -107,6 +111,7 @@ function FormField({
{description}
)}
+ {note && }
)
}
diff --git a/src/components/Form/FormFieldGroup.tsx b/src/components/Form/FormFieldGroup.tsx
index 6e4cb04..8cc2780 100644
--- a/src/components/Form/FormFieldGroup.tsx
+++ b/src/components/Form/FormFieldGroup.tsx
@@ -1,23 +1,40 @@
import { ReactNode } from 'react'
import { Text } from '../Text/index.js'
import { colors } from '../../utils/colors.js'
+import { radius } from '../../utils/radius.js'
type FormFieldGroupProps = {
label?: string
children: ReactNode
+ fullScreen?: boolean
}
-function FormFieldGroup({ label, children }: FormFieldGroupProps) {
+function FormFieldGroup({ label, children, fullScreen }: FormFieldGroupProps) {
return (
-
+
{label && (
{label}
-
+ {!fullScreen && (
+
+ )}
)}
diff --git a/src/components/Form/FormSidebar.tsx b/src/components/Form/FormSidebar.tsx
new file mode 100644
index 0000000..229f048
--- /dev/null
+++ b/src/components/Form/FormSidebar.tsx
@@ -0,0 +1,42 @@
+import { useContext, useEffect, useState } from 'react'
+import { FormContext } from './Form.js'
+import { Sidebar } from '../Sidebar/index.js'
+
+type FormSidebarProps = {}
+
+// TODO add customizable description and icon
+// TODO scroll into view when clicking on an item
+// TODO auto updating active based on scroll position
+function FormSidebar(props: FormSidebarProps) {
+ const [active, setActive] = useState
()
+ const ctx = useContext(FormContext)
+
+ if (!ctx) {
+ throw new Error('missing form context')
+ }
+
+ const { groups } = ctx
+
+ useEffect(() => {
+ if (!active) {
+ setActive(groups[0].label)
+ }
+ }, [groups])
+
+ return (
+
+
+
+ {groups.map((group) => (
+
+ {group.label}
+
+ ))}
+
+
+
+ )
+}
+
+export { FormSidebar }
+export type { FormSidebarProps }
diff --git a/src/components/Form/index.tsx b/src/components/Form/index.tsx
index 2fa5420..f7cb9be 100644
--- a/src/components/Form/index.tsx
+++ b/src/components/Form/index.tsx
@@ -2,4 +2,5 @@ export * from './useForm.js'
export * from './types.js'
export * from './FormField.js'
export * from './FormFieldGroup.js'
+export * from './FormSidebar.js'
export * from './Form.js'
diff --git a/src/components/Note/Note.stories.tsx b/src/components/Note/Note.stories.tsx
new file mode 100644
index 0000000..c22273d
--- /dev/null
+++ b/src/components/Note/Note.stories.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import { Note } from './index.js'
+import { Button } from '../Button/index.js'
+
+export default {
+ title: 'Note',
+ component: Note,
+}
+
+export const Default = {
+ args: {
+ title: 'Title',
+ description: 'Description',
+ children: ,
+ },
+}
+
+export const Red = {
+ args: {
+ title: 'Title',
+ description: 'Description',
+ children: ,
+ color: 'red',
+ },
+}
+
+export const Orange = {
+ args: {
+ title: 'Title',
+ description: 'Description',
+ children: ,
+ color: 'orange',
+ },
+}
+
+export const Green = {
+ args: {
+ title: 'Title',
+ description: 'Description',
+ children: ,
+ color: 'green',
+ },
+}
+
+export const Blue = {
+ args: {
+ title: 'Title',
+ description: 'Description',
+ children: ,
+ color: 'blue',
+ },
+}
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
new file mode 100644
index 0000000..8d223fa
--- /dev/null
+++ b/src/components/Note/index.tsx
@@ -0,0 +1,112 @@
+import { ReactNode } from 'react'
+import { Icon, IconProps } from '../Icon/index.js'
+import { radius } from '../../utils/radius.js'
+import { Text } from '../Text/index.js'
+import { Color, colors } from '../../utils/colors.js'
+
+type NoteProps = {
+ title: string
+ description?: string
+ icon?: IconProps['variant']
+ children?: ReactNode
+ color?: 'neutral' | 'orange' | 'red' | 'green' | 'blue'
+}
+
+function Note({
+ title,
+ description,
+ color = 'neutral',
+ icon = 'error',
+ children,
+}: NoteProps) {
+ return (
+
+ {icon &&
}
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {children && (
+
+ {children}
+
+ )}
+
+ )
+}
+
+export { Note }
+export type { NoteProps }
diff --git a/src/components/Patterns.stories.tsx b/src/components/Patterns.stories.tsx
index abdb172..8c8746c 100644
--- a/src/components/Patterns.stories.tsx
+++ b/src/components/Patterns.stories.tsx
@@ -487,15 +487,12 @@ export function App() {
},
{
key: 'name',
- type: 'title',
},
{
key: 'size',
- type: 'description',
},
{
key: 'statusText',
- type: 'description',
},
]}
/>
diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx
index f0e8828..40bfb04 100644
--- a/src/components/Sidebar/index.tsx
+++ b/src/components/Sidebar/index.tsx
@@ -15,31 +15,30 @@ type SidebarProps = {
value?: string
onChange: (value: string) => void
children: ReactNode
+ side?: 'left' | 'right'
}
-function Sidebar({ children, value, onChange }: SidebarProps) {
+function Sidebar({ children, value, onChange, side = 'left' }: SidebarProps) {
return (