Browse Source

Implement temporal components

Implement components with native HTML counterparts except datetime-local
master
TheoryOfNekomata 11 months ago
parent
commit
cc5757b459
26 changed files with 1535 additions and 41 deletions
  1. +3
    -3
      TODO.md
  2. +263
    -0
      categories/formatted/react/src/components/PatternTextInput/TextInput.test.tsx
  3. +232
    -0
      categories/formatted/react/src/components/PatternTextInput/index.tsx
  4. +1
    -0
      categories/formatted/react/src/index.ts
  5. +1
    -1
      categories/freeform/react/src/components/MaskedTextInput/index.tsx
  6. +1
    -1
      categories/freeform/react/src/components/MultilineTextInput/index.tsx
  7. +1
    -1
      categories/freeform/react/src/components/TextInput/index.tsx
  8. +5
    -2
      categories/number/react/package.json
  9. +1
    -1
      categories/number/react/scripts/build.ts
  10. +2
    -2
      categories/number/react/src/components/NumberSpinner/NumberSpinner.css
  11. +242
    -0
      categories/number/react/src/components/NumberSpinner/NumberSpinner.test.tsx
  12. +11
    -7
      categories/number/react/src/components/NumberSpinner/index.tsx
  13. +1
    -1
      categories/number/react/src/index.ts
  14. +11
    -0
      categories/number/react/src/web-number-react.test.ts
  15. +5
    -16
      categories/temporal/react/src/components/DateDropdown/index.tsx
  16. +2
    -0
      docs/00-philosophy.md
  17. +11
    -2
      pnpm-lock.yaml
  18. +237
    -0
      showcases/web-kitchensink-reactnext/src/components/temporal/TimeSpinner/index.tsx
  19. +215
    -0
      showcases/web-kitchensink-reactnext/src/components/temporal/YearMonthInput/index.tsx
  20. +215
    -0
      showcases/web-kitchensink-reactnext/src/components/temporal/YearWeekInput/index.tsx
  21. +3
    -0
      showcases/web-kitchensink-reactnext/src/components/temporal/index.ts
  22. +1
    -1
      showcases/web-kitchensink-reactnext/src/pages/_app.tsx
  23. +1
    -0
      showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx
  24. +23
    -2
      showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx
  25. +46
    -0
      showcases/web-kitchensink-reactnext/src/pages/categories/temporal/index.tsx
  26. +1
    -1
      showcases/web-kitchensink-reactnext/tailwind.config.js

+ 3
- 3
TODO.md View File

@@ -59,9 +59,9 @@
- [ ] DurationInput
- [ ] MonthInput
- [ ] MonthDayInput
- [ ] TimeSpinner
- [ ] YearMonthInput
- [ ] YearWeekInput
- [X] TimeSpinner
- [-] YearMonthInput
- [-] YearWeekInput
- [ ] YearInput

# Others


+ 263
- 0
categories/formatted/react/src/components/PatternTextInput/TextInput.test.tsx View File

@@ -0,0 +1,263 @@
import * as React from 'react';
import {
cleanup,
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TextControl } from '@tesseract-design/web-base';
import {
vi,
expect,
describe,
it,
afterEach,
} from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
import {
TextInput,
TextInputDerivedElement,
} from '.';

expect.extend(matchers);

describe('TextInput', () => {
afterEach(() => {
cleanup();
});

it('renders a textbox', () => {
render(
<TextInput />,
);
const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'text');
});

it('renders a border', () => {
render(
<TextInput
border
/>,
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<TextInput
label="foo"
/>,
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<TextInput
label="foo"
hiddenLabel
/>,
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeInTheDocument();
expect(label).toHaveClass('sr-only');
});

it('renders a hint', () => {
render(
<TextInput
hint="foo"
/>,
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<TextInput
indicator={
<div />
}
/>,
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each`
size | inputClassName | hintClassName | indicatorClassName
${'small'} | ${'h-10'} | ${'pr-10'} | ${'w-10'}
${'medium'} | ${'h-12'} | ${'pr-12'} | ${'w-12'}
${'large'} | ${'h-16'} | ${'pr-16'} | ${'w-16'}
`('on $size size', ({
size,
inputClassName,
hintClassName,
indicatorClassName,
}: {
size: TextControl.Size,
inputClassName: string,
hintClassName: string,
indicatorClassName: string,
}) => {
it('renders input styles', () => {
render(
<TextInput
size={size}
/>,
);

const input = screen.getByTestId('input');
expect(input).toHaveClass(inputClassName);
});

it('renders label styles with indicator', () => {
render(
<TextInput
size={size}
label="foo"
indicator={<div />}
/>,
);
const label = screen.getByTestId('label');
expect(label).toHaveClass(hintClassName);
});

it('renders hint styles when indicator is present', () => {
render(
<TextInput
size={size}
hint="hint"
indicator={<div />}
/>,
);

const hint = screen.getByTestId('hint');
expect(hint).toHaveClass(hintClassName);
});

it('renders indicator styles', () => {
render(
<TextInput
size={size}
indicator={
<div />
}
/>,
);

const indicator = screen.getByTestId('indicator');
expect(indicator).toHaveClass(indicatorClassName);
});
});

it('renders a block textbox', () => {
render(
<TextInput
block
/>,
);

const base = screen.getByTestId('base');
expect(base).toHaveClass('block');
});

it.each(TextControl.AVAILABLE_INPUT_TYPES)('renders a textbox with type %s', (inputType) => {
render(
<TextInput
type={inputType}
/>,
);
const textbox = screen.getByTestId('input');
expect(textbox).toHaveProperty('type', inputType);
});

it('falls back to text input mode when it clashes with the input type', () => {
render(
<TextInput
type="text"
inputMode="search"
/>,
);
const textbox = screen.getByTestId('input');
expect(textbox).toHaveProperty('inputMode', 'text');
});

describe.each`
variant | inputClassName | hintClassName
${'default'} | ${'pl-4'} | ${'bottom-0 pl-4 pb-1'}
${'alternate'} | ${'pl-1.5 pt-4'} | ${'top-0.5'}
`('on $variant style', ({
variant,
inputClassName,
hintClassName,
}: {
variant: TextControl.Variant,
inputClassName: string,
hintClassName: string,
}) => {
it('renders input styles', () => {
render(
<TextInput
variant={variant}
/>,
);

const input = screen.getByTestId('input');
expect(input).toHaveClass(inputClassName);
});

it('renders hint styles', () => {
render(
<TextInput
variant={variant}
hint="hint"
/>,
);

const hint = screen.getByTestId('hint');
expect(hint).toHaveClass(hintClassName);
});
});

it('handles change events', async () => {
const onChange = vi.fn().mockImplementationOnce(
(e: React.ChangeEvent<TextInputDerivedElement>) => {
e.preventDefault();
},
);
render(
<TextInput
onChange={onChange}
/>,
);
const textbox: HTMLInputElement = screen.getByRole('textbox');
await userEvent.type(textbox, 'foobar');
expect(onChange).toBeCalled();
});

it('handles input events', async () => {
const onInput = vi.fn().mockImplementationOnce(
(e: React.SyntheticEvent<TextInputDerivedElement>) => {
e.preventDefault();
},
);
render(
<TextInput
onInput={onInput}
/>,
);
const textbox: HTMLInputElement = screen.getByTestId('input');
await userEvent.type(textbox, 'foobar');
expect(onInput).toBeCalled();
});
});

+ 232
- 0
categories/formatted/react/src/components/PatternTextInput/index.tsx View File

@@ -0,0 +1,232 @@
import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';

export type PatternTextInputDerivedElement = HTMLInputElement;

export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControl.Size,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Type of the component value.
*/
type?: TextControl.InputType,
/**
* Style of the component.
*/
variant?: TextControl.Variant,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Input mode of the component.
*/
inputMode?: TextControl.InputMode,
}

/**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const PatternTextInput = React.forwardRef<PatternTextInputDerivedElement, PatternTextInputProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
type = 'text' as const,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
inputMode = type,
...etcProps
},
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

let resultInputMode = inputMode as React.HTMLProps<PatternTextInputDerivedElement>['inputMode'];
if (type === 'text' && resultInputMode === 'search') {
resultInputMode = 'text';
} else if (type === 'search' && resultInputMode === 'text') {
resultInputMode = 'search';
}

return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
ref={forwardedRef}
aria-labelledby={labelId}
id={id}
type={type}
inputMode={resultInputMode}
data-testid="input"
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{label && (
<label
data-testid="label"
htmlFor={id}
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
data-testid="indicator"
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)}
</div>
);
});

PatternTextInput.displayName = 'PatternTextInput';

PatternTextInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
type: 'text',
variant: 'default',
hiddenLabel: false,
inputMode: 'text',
};

+ 1
- 0
categories/formatted/react/src/index.ts View File

@@ -1,3 +1,4 @@
export * from './components/EmailInput';
export * from './components/PatternTextInput';
export * from './components/PhoneNumberInput';
export * from './components/UrlInput';

+ 1
- 1
categories/freeform/react/src/components/MaskedTextInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type MaskedTextInputDerivedElement = HTMLInputElement;

export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'size' | 'type' | 'label'> {
export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/


+ 1
- 1
categories/freeform/react/src/components/MultilineTextInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type MultilineTextInputDerivedElement = HTMLTextAreaElement;

export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode'> {
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/


+ 1
- 1
categories/freeform/react/src/components/TextInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type TextInputDerivedElement = HTMLInputElement;

export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> {
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/


+ 5
- 2
categories/number/react/package.json View File

@@ -15,8 +15,11 @@
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/node": "^18.14.1",
"@types/react": "^18.0.27",
"@types/testing-library__jest-dom": "^5.14.7",
"@vitest/coverage-v8": "^0.33.0",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"jsdom": "^21.1.0",
@@ -27,7 +30,7 @@
"tslib": "^2.5.0",
"tsx": "^3.12.7",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
"vitest": "^0.33.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
@@ -76,7 +79,7 @@
"types": "./dist/types/index.d.ts"
},
"./dist/Slider.css": "./dist/Slider.css",
"./dist/Spinner.css": "./dist/Spinner.css"
"./dist/NumberSpinner.css": "./dist/NumberSpinner.css"
},
"typesVersions": {
"*": {}


+ 1
- 1
categories/number/react/scripts/build.ts View File

@@ -17,4 +17,4 @@ const doCopy = (src: string, dest: string) => {
}

doCopy('./src/components/Slider/Slider.css', './dist/Slider.css');
doCopy('./src/components/Spinner/Spinner.css', './dist/Spinner.css');
doCopy('./src/components/NumberSpinner/NumberSpinner.css', './dist/NumberSpinner.css');

categories/number/react/src/components/Spinner/Spinner.css → categories/number/react/src/components/NumberSpinner/NumberSpinner.css View File

@@ -1,8 +1,8 @@
.tesseract-design-spinner {
.tesseract-design-number-spinner {
position: relative;
}

.tesseract-design-spinner::-webkit-inner-spin-button {
.tesseract-design-number-spinner::-webkit-inner-spin-button {
position: absolute;
top: 0;
right: 0;

+ 242
- 0
categories/number/react/src/components/NumberSpinner/NumberSpinner.test.tsx View File

@@ -0,0 +1,242 @@
import * as React from 'react';
import {
cleanup,
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TextControl } from '@tesseract-design/web-base';
import {
vi,
expect,
describe,
it,
afterEach,
} from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
import {
NumberSpinner,
NumberSpinnerDerivedElement,
} from '.';

expect.extend(matchers);

describe('NumberSpinner', () => {
afterEach(() => {
cleanup();
});

it('renders a number input', () => {
render(
<NumberSpinner />,
);
const textbox = screen.getByTestId('input');
expect(textbox).toBeInTheDocument();
expect(textbox).toHaveProperty('type', 'number');
});

it('renders a border', () => {
render(
<NumberSpinner
border
/>,
);
const border = screen.getByTestId('border');
expect(border).toBeInTheDocument();
});

it('renders a label', () => {
render(
<NumberSpinner
label="foo"
/>,
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.getByTestId('label');
expect(label).toHaveTextContent('foo');
});

it('renders a hidden label', () => {
render(
<NumberSpinner
label="foo"
hiddenLabel
/>,
);
const textbox = screen.getByLabelText('foo');
expect(textbox).toBeInTheDocument();
const label = screen.queryByTestId('label');
expect(label).toBeInTheDocument();
expect(label).toHaveClass('sr-only');
});

it('renders a hint', () => {
render(
<NumberSpinner
hint="foo"
/>,
);
const hint = screen.getByTestId('hint');
expect(hint).toBeInTheDocument();
});

it('renders an indicator', () => {
render(
<NumberSpinner
indicator={
<div />
}
/>,
);
const indicator = screen.getByTestId('indicator');
expect(indicator).toBeInTheDocument();
});

describe.each`
size | inputClassName | hintClassName | indicatorClassName
${'small'} | ${'h-10'} | ${'pr-10'} | ${'w-10'}
${'medium'} | ${'h-12'} | ${'pr-12'} | ${'w-12'}
${'large'} | ${'h-16'} | ${'pr-16'} | ${'w-16'}
`('on $size size', ({
size,
inputClassName,
hintClassName,
indicatorClassName,
}: {
size: TextControl.Size,
inputClassName: string,
hintClassName: string,
indicatorClassName: string,
}) => {
it('renders input styles', () => {
render(
<NumberSpinner
size={size}
/>,
);

const input = screen.getByTestId('input');
expect(input).toHaveClass(inputClassName);
});

it('renders label styles with indicator', () => {
render(
<NumberSpinner
size={size}
label="foo"
indicator={<div />}
/>,
);
const label = screen.getByTestId('label');
expect(label).toHaveClass(hintClassName);
});

it('renders hint styles when indicator is present', () => {
render(
<NumberSpinner
size={size}
hint="hint"
indicator={<div />}
/>,
);

const hint = screen.getByTestId('hint');
expect(hint).toHaveClass(hintClassName);
});

it('renders indicator styles', () => {
render(
<NumberSpinner
size={size}
indicator={
<div />
}
/>,
);

const indicator = screen.getByTestId('indicator');
expect(indicator).toHaveClass(indicatorClassName);
});
});

it('renders a block textbox', () => {
render(
<NumberSpinner
block
/>,
);

const base = screen.getByTestId('base');
expect(base).toHaveClass('block');
});

describe.each`
variant | inputClassName | hintClassName
${'default'} | ${'pl-4'} | ${'bottom-0 pl-4 pb-1'}
${'alternate'} | ${'pl-1.5 pt-4'} | ${'top-0.5'}
`('on $variant style', ({
variant,
inputClassName,
hintClassName,
}: {
variant: TextControl.Variant,
inputClassName: string,
hintClassName: string,
}) => {
it('renders input styles', () => {
render(
<NumberSpinner
variant={variant}
/>,
);

const input = screen.getByTestId('input');
expect(input).toHaveClass(inputClassName);
});

it('renders hint styles', () => {
render(
<NumberSpinner
variant={variant}
hint="hint"
/>,
);

const hint = screen.getByTestId('hint');
expect(hint).toHaveClass(hintClassName);
});
});

it('handles change events', async () => {
const onChange = vi.fn().mockImplementationOnce(
(e: React.ChangeEvent<NumberSpinnerDerivedElement>) => {
e.preventDefault();
},
);
render(
<NumberSpinner
onChange={onChange}
/>,
);
const textbox = screen.getByTestId('input');
await userEvent.type(textbox, '69420');
expect(onChange).toBeCalled();
});

it('handles input events', async () => {
const onInput = vi.fn().mockImplementationOnce(
(e: React.SyntheticEvent<NumberSpinnerDerivedElement>) => {
e.preventDefault();
},
);
render(
<NumberSpinner
onInput={onInput}
/>,
);
const textbox = screen.getByTestId('input');
await userEvent.type(textbox, '69420');
expect(onInput).toBeCalled();
});
});

categories/number/react/src/components/Spinner/index.tsx → categories/number/react/src/components/NumberSpinner/index.tsx View File

@@ -2,9 +2,9 @@ import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';

export type SpinnerDerivedElement = HTMLInputElement;
export type NumberSpinnerDerivedElement = HTMLInputElement;

export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement>, 'size' | 'type' | 'label'> {
export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDerivedElement>, 'size' | 'type' | 'label'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -42,7 +42,7 @@ export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement
/**
* Component for inputting numeric values.
*/
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, NumberSpinnerProps>((
{
label,
hint,
@@ -54,8 +54,9 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: SpinnerProps,
}: NumberSpinnerProps,
forwardedRef,
) => {
const labelId = React.useId();
@@ -73,6 +74,8 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
},
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
@@ -85,7 +88,7 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
'bg-negative rounded-inherit w-full peer block tabular-nums font-inherit',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'tesseract-design-spinner',
'tesseract-design-number-spinner',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
@@ -171,6 +174,7 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
)}
{indicator && (
<div
data-testid="indicator"
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
@@ -193,9 +197,9 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
);
});

Spinner.displayName = 'Spinner';
NumberSpinner.displayName = 'NumberSpinner';

Spinner.defaultProps = {
NumberSpinner.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,

+ 1
- 1
categories/number/react/src/index.ts View File

@@ -1,2 +1,2 @@
export * from './components/Slider';
export * from './components/Spinner';
export * from './components/NumberSpinner';

+ 11
- 0
categories/number/react/src/web-number-react.test.ts View File

@@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest';
import * as WebNumberReact from '.';

describe('web-number-react', () => {
it.each([
'NumberSpinner',
'Slider',
])('exports %s', (namedExport) => {
expect(WebNumberReact).toHaveProperty(namedExport);
});
});

+ 5
- 16
categories/temporal/react/src/components/DateDropdown/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type DateDropdownDerivedElement = HTMLInputElement;

export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label'> {
export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -58,8 +58,6 @@ export const DateDropdown = React.forwardRef<
className,
id: idProp,
style,
onFocus,
onClick,
...etcProps
}: DateDropdownProps,
forwardedRef,
@@ -68,16 +66,6 @@ export const DateDropdown = React.forwardRef<
const defaultId = React.useId();
const id = idProp ?? defaultId;

const handleFocus: React.FocusEventHandler<DateDropdownDerivedElement> = (e) => {
e.currentTarget.showPicker();
onFocus?.(e);
};

const handleClick: React.MouseEventHandler<DateDropdownDerivedElement> = (e) => {
e.currentTarget.showPicker();
onClick?.(e);
};

return (
<div
className={clsx(
@@ -90,6 +78,7 @@ export const DateDropdown = React.forwardRef<
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
@@ -98,10 +87,9 @@ export const DateDropdown = React.forwardRef<
aria-labelledby={labelId}
type="date"
data-testid="input"
onFocus={handleFocus}
onClick={handleClick}
pattern="\d{4}-\d{2}-\d{2}"
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit',
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
@@ -189,6 +177,7 @@ export const DateDropdown = React.forwardRef<
)}
{indicator && (
<div
data-testid="indicator"
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{


+ 2
- 0
docs/00-philosophy.md View File

@@ -34,3 +34,5 @@
elements may have different markup and appearance. Tesseract
ensures components are still usable on most assistive technologies given
both versions of the user interface.
- All components are named in order to depict the purpose, the nature of data
being operated, and UX expectations.

+ 11
- 2
pnpm-lock.yaml View File

@@ -579,12 +579,21 @@ importers:
'@testing-library/react':
specifier: ^13.4.0
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
'@testing-library/user-event':
specifier: ^14.4.3
version: 14.4.3(@testing-library/dom@8.20.1)
'@types/node':
specifier: ^18.14.1
version: 18.14.1
'@types/react':
specifier: ^18.0.27
version: 18.2.14
'@types/testing-library__jest-dom':
specifier: ^5.14.7
version: 5.14.7
'@vitest/coverage-v8':
specifier: ^0.33.0
version: 0.33.0(vitest@0.33.0)
eslint:
specifier: ^8.35.0
version: 8.43.0
@@ -616,8 +625,8 @@ importers:
specifier: ^4.9.5
version: 4.9.5
vitest:
specifier: ^0.28.1
version: 0.28.1(jsdom@21.1.0)
specifier: ^0.33.0
version: 0.33.0(jsdom@21.1.0)

categories/temporal/react:
dependencies:


+ 237
- 0
showcases/web-kitchensink-reactnext/src/components/temporal/TimeSpinner/index.tsx View File

@@ -0,0 +1,237 @@
import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';

export type TimeSpinnerDerivedElement = HTMLInputElement;

type Digit = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9);

type Segment = `${Digit}${Digit}`;

type StepHhMm = `${Segment}:${Segment}`;

type StepHhMmSs = `${StepHhMm}:${Segment}`;

type Step = StepHhMm | StepHhMmSs;

export interface TimeSpinnerProps extends Omit<React.HTMLProps<TimeSpinnerDerivedElement>, 'size' | 'type' | 'label' | 'step' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControl.Size,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
variant?: TextControl.Variant,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Should the component display seconds?
*/
displaySeconds?: boolean,
/**
* Step size for the component.
*/
step?: Step,
}

/**
* Component for inputting textual values.
*/
export const TimeSpinner = React.forwardRef<
TimeSpinnerDerivedElement,
TimeSpinnerProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
displaySeconds = false,
step = '00:01',
...etcProps
}: TimeSpinnerProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

const [hh, mm, ss = 0] = step.split(':').map((s: string) => parseInt(s, 10));
const stepValue = ss + (mm * 60) + (hh * 3600);

return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="time"
data-testid="input"
step={displaySeconds && stepValue > 60 ? 1 : stepValue}
pattern="\d{2}:\d{2}(:\d{2})?"
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)}
</div>
);
});

TimeSpinner.displayName = 'TimeSpinner';

TimeSpinner.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 215
- 0
showcases/web-kitchensink-reactnext/src/components/temporal/YearMonthInput/index.tsx View File

@@ -0,0 +1,215 @@
import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';

export type YearMonthInputDerivedElement = HTMLInputElement;

export interface YearMonthInputProps extends Omit<React.HTMLProps<YearMonthInputDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControl.Size,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
variant?: TextControl.Variant,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting textual values.
*/
export const YearMonthInput = React.forwardRef<
YearMonthInputDerivedElement,
YearMonthInputProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: YearMonthInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

// TODO render enhanced version

return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="month"
data-testid="input"
pattern="\d{4}-\d{2}"
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)}
</div>
);
});

YearMonthInput.displayName = 'YearMonthInput';

YearMonthInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 215
- 0
showcases/web-kitchensink-reactnext/src/components/temporal/YearWeekInput/index.tsx View File

@@ -0,0 +1,215 @@
import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';

export type YearWeekInputDerivedElement = HTMLInputElement;

export interface YearWeekInputProps extends Omit<React.HTMLProps<YearWeekInputDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Size of the component.
*/
size?: TextControl.Size,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Style of the component.
*/
variant?: TextControl.Variant,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

/**
* Component for inputting textual values.
*/
export const YearWeekInput = React.forwardRef<
YearWeekInputDerivedElement,
YearWeekInputProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: YearWeekInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

// TODO render enhanced version

return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
data-testid="base"
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="week"
data-testid="input"
pattern="\d{4}-W\d{,2}"
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)}
</div>
);
});

YearWeekInput.displayName = 'YearWeekInput';

YearWeekInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 3
- 0
showcases/web-kitchensink-reactnext/src/components/temporal/index.ts View File

@@ -0,0 +1,3 @@
export * from './TimeSpinner';
export * from './YearMonthInput';
export * from './YearWeekInput';

+ 1
- 1
showcases/web-kitchensink-reactnext/src/pages/_app.tsx View File

@@ -12,8 +12,8 @@ import '@tesseract-design/web-multichoice-react/dist/ToggleButton.css'
import '@tesseract-design/web-multichoice-react/dist/ToggleSwitch.css'
import '@tesseract-design/web-multichoice-react/dist/ToggleTickBox.css'

import '@tesseract-design/web-number-react/dist/NumberSpinner.css'
import '@tesseract-design/web-number-react/dist/Slider.css'
import '@tesseract-design/web-number-react/dist/Spinner.css'

import '@modal-sh/react-refractor/dist/Refractor.css'



+ 1
- 0
showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx View File

@@ -12,6 +12,7 @@ const TemporalPage: NextPage = () => {
label="Phone"
name="phone"
enhanced
border
onFocus={(e) => { console.log('focus', e.currentTarget)}}
onBlur={(e) => { console.log('blur', e.currentTarget)}}
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}}


+ 23
- 2
showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx View File

@@ -6,9 +6,9 @@ import {Section, Subsection} from '@/components/Section';
const NumberPage: NextPage = () => {
return (
<main className="my-16 md:my-32">
<Section title="Spinner">
<Section title="NumberSpinner">
<Subsection title="Default">
<TesseractNumber.Spinner
<TesseractNumber.NumberSpinner
min={-100}
max={100}
step="any"
@@ -46,6 +46,27 @@ const NumberPage: NextPage = () => {
</TesseractNumber.Slider>
</Subsection>
<Subsection title="Vertical">
<TesseractNumber.Slider
min={-100}
max={100}
orient="vertical"
>
<optgroup label="Test Values">
<option value={-100}>
Lowest
</option>
<option value={25}>
日本語
</option>
<option value={50} />
<option value={100}>
Highest
</option>
<option value={200}>
Out of bounds
</option>
</optgroup>
</TesseractNumber.Slider>
<TesseractNumber.Slider
min={-100}
max={100}


+ 46
- 0
showcases/web-kitchensink-reactnext/src/pages/categories/temporal/index.tsx View File

@@ -2,6 +2,7 @@ import {NextPage} from 'next';
import {DefaultLayout} from '@/components/DefaultLayout';
import {Section, Subsection} from '@/components/Section';
import * as Temporal from '@tesseract-design/web-temporal-react';
import * as TemporalWip from '@/components/temporal';

const TemporalPage: NextPage = () => {
return (
@@ -10,6 +11,51 @@ const TemporalPage: NextPage = () => {
<Subsection title="Default">
<Temporal.DateDropdown
label="Birthday"
border
/>
</Subsection>
</Section>
<Section title="TimeSpinner">
<Subsection title="Default">
<TemporalWip.TimeSpinner
label="Time"
variant="default"
border
/>
</Subsection>
<Subsection title="Step">
<TemporalWip.TimeSpinner
label="Time"
variant="default"
border
step="00:15:00"
/>
</Subsection>
<Subsection title="Step + Display Seconds">
<TemporalWip.TimeSpinner
label="Time"
variant="default"
border
step="00:00:05"
displaySeconds
/>
</Subsection>
</Section>
<Section title="YearMonthInput">
<Subsection title="Default">
<TemporalWip.YearMonthInput
label="Month"
variant="default"
border
/>
</Subsection>
</Section>
<Section title="YearWeekInput">
<Subsection title="Default">
<TemporalWip.YearWeekInput
label="Vacation"
variant="default"
border
/>
</Subsection>
</Section>


+ 1
- 1
showcases/web-kitchensink-reactnext/tailwind.config.js View File

@@ -6,7 +6,7 @@ module.exports = {
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/categories/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@tesseract-design/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@tesseract-design/web-*-react/dist/**/*.js',
],
theme: {
fontFamily: {


Loading…
Cancel
Save