ソースを参照

Improve a11y, styling for preview components

Style sliders and add active indicators for interactable components
pull/1/head
コミット
4e8a0ae6a1
9個のファイルの変更295行の追加86行の削除
  1. +77
    -26
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx
  2. +88
    -30
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx
  3. +36
    -0
      packages/web-kitchensink-reactnext/src/categories/number/react/components/Slider/index.tsx
  4. +1
    -0
      packages/web-kitchensink-reactnext/src/categories/number/react/index.ts
  5. +6
    -1
      packages/web-kitchensink-reactnext/src/pages/_document.tsx
  6. +59
    -28
      packages/web-kitchensink-reactnext/src/styles/globals.css
  7. +22
    -0
      packages/web-kitchensink-reactnext/src/styles/theme.ts
  8. +4
    -1
      packages/web-kitchensink-reactnext/tailwind.config.js
  9. +2
    -0
      packages/web-kitchensink-reactnext/tsconfig.json

+ 77
- 26
packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx ファイルの表示

@@ -6,11 +6,13 @@ import {
formatSecondsDurationConcise,
formatSecondsDurationPrecise,
} from '@/utils/numeral';
import theme from '@/styles/theme';
import {useMediaControls} from '../../hooks/interactive';
import {useAugmentedFile} from '@/categories/blob/react';

import clsx from 'clsx';
import {WaveSurferCanvas} from '@/packages/react-wavesurfer';
import {Slider} from '@/categories/number/react';

type AudioFilePreviewDerivedComponent = HTMLAudioElement;

@@ -99,53 +101,59 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
</audio>
{visualizationMode === 'waveform' && (
<WaveSurferCanvas
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-[#000000]"
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10"
ref={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
waveColor="rgb(199 138 179)"
progressColor="rgb(255 153 0)"
waveColor={`rgb(${theme.primary.split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
progressColor={`rgb(${theme.primary})`}
interact
/>
)}
<div className="flex gap-4 absolute top-0 right-0 z-[2] px-4">
<label
className={clsx(
'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="waveform"
className="sr-only"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
/>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
Waveform
</span>
</label>
<label
className={clsx(
'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'h-12 flex items-center justify-center leading-none gap-4 select-none',
)}
>
<input
type="radio"
name="visualizationMode"
value="spectrum"
className="sr-only"
className="sr-only peer/waveform"
onChange={handleVisualizationModeChange}
/>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
className={clsx(
'block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded text-primary',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
Spectrum
</span>
@@ -153,19 +161,59 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
</div>
</div>
{enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4 relative">
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center">
<button
className="w-10 h-full flex-shrink-0 text-primary"
className={clsx(
'w-10 h-full flex-shrink-0 text-primary flex items-center justify-center',
'focus:text-secondary focus:outline-0',
'active:text-tertiary',
'disabled:text-primary disabled:cursor-not-allowed disabled:opacity-50',
)}
type="submit"
name="action"
value="togglePlayback"
form={formId}
>
{isPlaying ? '⏸' : '▶'}
{
isPlaying
? (
<svg
aria-label="Pause"
viewBox="0 0 24 24"
className="w-6 h-6 fill-none stroke-current stroke-2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect
x="6"
y="4"
width="4"
height="16"
/>
<rect
x="14"
y="4"
width="4"
height="16"
/>
</svg>
)
: (
<svg
aria-label="Play"
viewBox="0 0 24 24"
className="w-6 h-6 fill-none stroke-current stroke-2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)
}
</button>
<div className="flex-auto w-full flex items-center gap-2 text-sm relative">
<button
className="absolute overflow-hidden w-12 opacity-0 h-10"
className="absolute overflow-hidden w-12 opacity-0 h-10 peer/seek"
title="Toggle Seek Time Count Mode"
type="submit"
name="action"
@@ -174,8 +222,13 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
>
Toggle Seek Time Count Mode
</button>
<span
className="before:block before:content-[attr(title)] contents tabular-nums"
<div
className={clsx(
'before:block before:content-[attr(title)] contents tabular-nums flex-auto text-primary font-bold',
'peer-focus/seek:text-secondary peer-focus/seek:outline-0',
'peer-active/seek:text-tertiary',
'peer-disabled/seek:text-primary \'peer-disabled/seek:cursor-not-allowed \'peer-disabled/seek:opacity-50'
)}
title={
`${
isSeekTimeCountingDown ? '−' : '+'
@@ -186,9 +239,8 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
}`
}
>
<input
type="range"
className="flex-auto w-full tabular-nums"
<Slider
className="w-full bg-negative text-base"
ref={seekRef}
onMouseDown={startSeek}
onMouseUp={endSeek}
@@ -196,13 +248,12 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
defaultValue="0"
step="any"
/>
</span>
<span>
</div>
<span className="tabular-nums font-bold">
{formatSecondsDurationConcise(durationDisplay)}
</span>
</div>
<input
type="range"
<Slider
ref={volumeRef}
max={1}
min={0}


+ 88
- 30
packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx ファイルの表示

@@ -3,6 +3,7 @@ import {augmentVideoFile, getMimeTypeDescription} from '@/utils/blob';
import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral';
import {useAugmentedFile, useMediaControls} from '@tesseract-design/web-blob-react';
import clsx from 'clsx';
import {Slider} from '@tesseract-design/web-number-react';

type VideoFilePreviewDerivedComponent = HTMLVideoElement;

@@ -72,37 +73,91 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
data-testid="preview"
>
<div className="w-full flex-auto relative">
<video
{...etcProps}
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-[#000000]"
ref={mediaControllerRef}
onLoadedMetadata={refreshControls}
onDurationChange={refreshControls}
onEnded={reset}
onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
controls={!enhanced}
>
<source
src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type}
/>
</video>
<video
{...etcProps}
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10"
ref={mediaControllerRef}
onLoadedMetadata={refreshControls}
onDurationChange={refreshControls}
onEnded={reset}
onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
controls={!enhanced}
>
<source
src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type}
/>
</video>
<button
className="absolute w-full h-full top-0 left-0"
type="submit"
name="action"
value="togglePlayback"
form={formId}
tabIndex={-1}
>
<span className="sr-only">
{isPlaying ? 'Pause' : 'Play'}
</span>
</button>
</div>
{enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4">
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center">
<button
className="w-10 h-full flex-shrink-0 text-primary"
className={
clsx(
'w-10 h-full flex-shrink-0 text-primary flex items-center justify-center',
'focus:text-secondary focus:outline-0',
'active:text-tertiary',
'disabled:text-primary disabled:cursor-not-allowed disabled:opacity-50'
)
}
type="submit"
name="action"
value="togglePlayback"
form={formId}
>
{isPlaying ? '⏸' : '▶'}
{
isPlaying
? (
<svg
aria-label="Pause"
viewBox="0 0 24 24"
className="w-6 h-6 fill-none stroke-current stroke-2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect
x="6"
y="4"
width="4"
height="16"
/>
<rect
x="14"
y="4"
width="4"
height="16"
/>
</svg>
)
: (
<svg
aria-label="Play"
viewBox="0 0 24 24"
className="w-6 h-6 fill-none stroke-current stroke-2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)
}
</button>
<div className="flex-auto w-full flex items-center gap-2 text-sm relative">
<button
className="absolute overflow-hidden w-12 opacity-0 h-10"
className="absolute overflow-hidden w-12 opacity-0 h-10 peer/seek"
title="Toggle Seek Time Count Mode"
type="submit"
name="action"
@@ -111,8 +166,13 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
>
Toggle Seek Time Count Mode
</button>
<span
className="before:block before:content-[attr(title)] contents tabular-nums"
<div
className={clsx(
'before:block before:content-[attr(title)] contents tabular-nums flex-auto text-primary font-bold',
'peer-focus/seek:text-secondary peer-focus/seek:outline-0',
'peer-active/seek:text-tertiary',
'peer-disabled/seek:text-primary \'peer-disabled/seek:cursor-not-allowed \'peer-disabled/seek:opacity-50'
)}
title={
`${
isSeekTimeCountingDown ? '−' : '+'
@@ -123,9 +183,8 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
}`
}
>
<input
type="range"
className="flex-auto w-full tabular-nums"
<Slider
className="w-full bg-negative text-base"
ref={seekRef}
onMouseDown={startSeek}
onMouseUp={endSeek}
@@ -133,13 +192,12 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
defaultValue="0"
step="any"
/>
</div>
<span className="tabular-nums font-bold">
{formatSecondsDurationConcise(durationDisplay)}
</span>
<span>
{formatSecondsDurationConcise(durationDisplay)}
</span>
</div>
<input
type="range"
<Slider
ref={volumeRef}
max={1}
min={0}


+ 36
- 0
packages/web-kitchensink-reactnext/src/categories/number/react/components/Slider/index.tsx ファイルの表示

@@ -0,0 +1,36 @@
import * as React from 'react';
import clsx from 'clsx';

type SliderDerivedComponent = HTMLInputElement;

export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'type'> {

}

export const Slider = React.forwardRef<SliderDerivedComponent, SliderProps>(({
className,
style,
...etcProps
}, forwardedRef) => {
return (
<div
className={clsx(
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
type="range"
className={clsx(
'w-full h-full bg-inherit slider block text-primary ring-secondary/50',
'focus:text-secondary focus:outline-0 focus:ring-4',
'active:text-tertiary active:ring-tertiary/50',
)}
/>
</div>
)
});

Slider.displayName = 'Slider';

+ 1
- 0
packages/web-kitchensink-reactnext/src/categories/number/react/index.ts ファイルの表示

@@ -0,0 +1 @@
export * from './components/Slider';

+ 6
- 1
packages/web-kitchensink-reactnext/src/pages/_document.tsx ファイルの表示

@@ -1,9 +1,14 @@
import { Html, Head, Main, NextScript } from 'next/document'
import theme from '@/styles/theme'

export default function Document() {
return (
<Html lang="en" className="bg-negative text-positive">
<Head />
<Head>
<style>
{`:root{${Object.entries(theme).map(([key, value]) => `--color-${key}: ${value};`).join('')}}`}
</style>
</Head>
<body>
<Main />
<NextScript />


+ 59
- 28
packages/web-kitchensink-reactnext/src/styles/globals.css ファイルの表示

@@ -1,41 +1,72 @@
@tailwind base;
@tailwind utilities;

@layer base {
:root {
--color-shade: 0 0 0;
--color-negative: 34 34 34;
--color-positive: 238 238 238;
--color-primary: 199 138 179;
--color-secondary: 255 153 0;
--color-tertiary: 215 95 75;
--color-code-number: 116 249 94;
--color-code-keyword: 255 67 137;
--color-code-type: 80 151 210;
--color-code-instance-attribute: 118 167 210;
--color-code-function: 103 194 82;
--color-code-parameter: 145 94 194;
--color-code-property: 255 161 201;
--color-code-string: 238 211 113;
--color-code-variable: 139 194 117;
--color-code-regexp: 116 167 43;
--color-code-url: 0 153 204;
--color-code-global: 194 128 80;
@layer utilities {
.font-condensed {
font-stretch: condensed;
}

.font-semi-condensed {
font-stretch: semi-condensed;
}

.font-expanded {
font-stretch: expanded;
}

.font-semi-expanded {
font-stretch: semi-expanded;
}
}

.slider {
@apply rounded-full;
appearance: none;
cursor: pointer;
position: relative;
overflow: hidden;
height: 1em;
color: rgb(var(--color-primary));
}

.slider::before {
@apply rounded-full;
content: '';
display: block;
position: absolute;
background-color: currentColor;
opacity: 0.5;
width: 100%;
height: 50%;
top: 25%;
}

.slider::-webkit-slider-container {
height: 100%;
overflow: hidden;
box-sizing: border-box;
}

.font-condensed {
font-stretch: condensed;
.slider::-webkit-slider-runnable-track {
appearance: none;
height: 100%;
}

.font-semi-condensed {
font-stretch: semi-condensed;
.slider::-webkit-slider-thumb {
@apply rounded-full;
background-color: currentColor;
appearance: none;
height: 100%;
aspect-ratio: 1 / 1;
z-index: 1;
position: relative;
box-shadow: -100000.5em 0 0 100000em rgb(var(--color-primary) / 50%);
}

.font-expanded {
font-stretch: expanded;
.slider:focus::-webkit-slider-thumb {
box-shadow: -100000.5em 0 0 100000em rgb(var(--color-secondary) / 50%);
}

.font-semi-expanded {
font-stretch: semi-expanded;
.slider:active::-webkit-slider-thumb {
box-shadow: -100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%);
}

+ 22
- 0
packages/web-kitchensink-reactnext/src/styles/theme.ts ファイルの表示

@@ -0,0 +1,22 @@
const theme = {
"shade": "0 0 0",
"negative": "34 34 34",
"positive": "238 238 238",
"primary": "199 138 179",
"secondary": "255 153 0",
"tertiary": "215 95 75",
"code-number": "116 249 94",
"code-keyword": "255 67 137",
"code-type": "80 151 210",
"code-instance-attribute": "118 167 210",
"code-function": "103 194 82",
"code-parameter": "145 94 194",
"code-property": "255 161 201",
"code-string": "238 211 113",
"code-variable": "139 194 117",
"code-regexp": "116 167 43",
"code-url": "0 153 204",
"code-global": "194 128 80"
} as const;

export default theme;

+ 4
- 1
packages/web-kitchensink-reactnext/tailwind.config.js ファイルの表示

@@ -1,3 +1,5 @@
const defaultTheme = require('tailwindcss/defaultTheme')

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
@@ -7,7 +9,8 @@ module.exports = {
],
theme: {
fontFamily: {
sans: ['Encode Sans Semi Expanded', 'Encode Sans', 'sans-serif'],
sans: ['Encode Sans', ...defaultTheme.fontFamily.sans],
mono: ['MonoLisa', 'mononoki', ...defaultTheme.fontFamily.mono],
},
colors: {
'shade': 'rgb(var(--color-shade))',


+ 2
- 0
packages/web-kitchensink-reactnext/tsconfig.json ファイルの表示

@@ -16,6 +16,7 @@
"incremental": true,
"paths": {
"@/*": ["./src/*"],
"tailwind.config": ["./tailwind.config.js"],
"@tesseract-design/web-base-button": ["./src/base/button"],
"@tesseract-design/web-base-selectcontrol": ["./src/base/selectcontrol"],
"@tesseract-design/web-base-textcontrol": ["./src/base/textcontrol"],
@@ -24,6 +25,7 @@
"@tesseract-design/web-freeform-react": ["./src/categories/freeform/react"],
"@tesseract-design/web-information-react": ["./src/categories/information/react"],
"@tesseract-design/web-option-react": ["./src/categories/option/react"],
"@tesseract-design/web-number-react": ["./src/categories/number/react"],
"@tesseract-design/web-navigation-react": ["./src/categories/navigation/react"],
}
},


読み込み中…
キャンセル
保存