Remove glows, refine feature legend, forms, and showcase pages
- Delete glows.css and GlowsPage (unused) - Enhance DTFeatureLegend and feature-legend.css with improved layout - Update DTRadioGroup styling - Refine bevels.css, forms-dt.css - Update AnimationsPage, CardsAdvancedPage, FormsPage, HomePage - Add type:module to web package.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
/**
|
||||
* DTFeatureLegend — Grid of product feature icons with rotated labels.
|
||||
* DTFeatureLegend — Interactive grid of product feature icons with rotated labels.
|
||||
*
|
||||
* CSS reference: feature-legend.css .dt-feature-legend, .dt-feature-legend-header,
|
||||
* .dt-feature-legend-grid, .dt-feature-legend-item, .dt-feature-supported, etc.
|
||||
* Matches dt-shopify-storefront UseCaseLegend: header bar with hover details,
|
||||
* rotated vertical labels (above for row 0, below for row 1), ? toggle,
|
||||
* bordered grid with dark cell backgrounds, and pulse-on-hover icons.
|
||||
*
|
||||
* CSS reference: feature-legend.css
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import { useState, type ReactNode, type CSSProperties } from 'react';
|
||||
import type { DTVariant } from '@dangerousthings/tokens';
|
||||
import { cx } from '../utils/cx';
|
||||
import { getVariantClass, featureStateToVariant } from '../utils/variantClasses';
|
||||
import { getVariantClass } from '../utils/variantClasses';
|
||||
|
||||
export interface DTFeatureItem {
|
||||
key: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
state: 'supported' | 'disabled' | 'unsupported';
|
||||
/** Optional detail text shown in the header on hover */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const stateClassMap: Record<DTFeatureItem['state'], string> = {
|
||||
@@ -32,6 +37,8 @@ interface DTFeatureLegendProps {
|
||||
columns?: number;
|
||||
/** Icon size in px @default 42 */
|
||||
iconSize?: number;
|
||||
/** Show rotated labels initially @default true */
|
||||
showLabels?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
@@ -42,35 +49,93 @@ export function DTFeatureLegend({
|
||||
variant = 'normal',
|
||||
columns = 5,
|
||||
iconSize = 42,
|
||||
showLabels: initialShowLabels = true,
|
||||
className,
|
||||
style,
|
||||
}: DTFeatureLegendProps) {
|
||||
// Suppress unused import warning — featureStateToVariant is available for consumers
|
||||
void featureStateToVariant;
|
||||
const [showLabels, setShowLabels] = useState(initialShowLabels);
|
||||
const [hoveredFeature, setHoveredFeature] = useState<string | null>(null);
|
||||
|
||||
const hoveredItem = hoveredFeature
|
||||
? features.find(f => f.key === hoveredFeature)
|
||||
: null;
|
||||
|
||||
const rowCount = Math.ceil(features.length / columns);
|
||||
const hasBottomRow = rowCount >= 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('dt-feature-legend', getVariantClass(variant), className)}
|
||||
style={style}>
|
||||
|
||||
{/* Header bar — title + hovered feature detail + ? toggle */}
|
||||
{title && (
|
||||
<div className="dt-feature-legend-header">{title}</div>
|
||||
)}
|
||||
<div
|
||||
className="dt-feature-legend-grid"
|
||||
style={{ '--dt-feature-columns': columns } as CSSProperties}>
|
||||
{features.map(feature => (
|
||||
<div
|
||||
key={feature.key}
|
||||
className={cx('dt-feature-legend-item', stateClassMap[feature.state])}
|
||||
style={{ width: `${100 / columns}%` }}>
|
||||
<div
|
||||
className="dt-feature-legend-icon"
|
||||
style={{ width: `${iconSize}px`, height: `${iconSize}px` }}>
|
||||
{feature.icon}
|
||||
<div className="dt-feature-legend-header">
|
||||
<div className="dt-feature-legend-header-content">
|
||||
<div className="dt-feature-legend-title">{title}</div>
|
||||
<div className="dt-feature-legend-detail">
|
||||
{hoveredItem ? (
|
||||
<span className={stateClassMap[hoveredItem.state]}>
|
||||
{hoveredItem.name}{hoveredItem.detail ? ': ' : ''}
|
||||
{hoveredItem.detail && <strong>{hoveredItem.detail}</strong>}
|
||||
</span>
|
||||
) : (
|
||||
<span className="dt-feature-legend-hint">
|
||||
Hover a feature for details
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="dt-feature-legend-label">{feature.name}</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="dt-feature-legend-toggle"
|
||||
onClick={() => setShowLabels(v => !v)}
|
||||
aria-label={showLabels ? 'Hide feature labels' : 'Show feature labels'}
|
||||
title={showLabels ? 'Hide labels' : 'Show labels'}
|
||||
type="button">
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Icon grid with border */}
|
||||
<div className="dt-feature-legend-body">
|
||||
<div
|
||||
className={cx(
|
||||
'dt-feature-legend-grid',
|
||||
showLabels && 'with-labels',
|
||||
showLabels && hasBottomRow && 'with-labels-bottom',
|
||||
)}
|
||||
style={{ '--dt-feature-columns': columns } as CSSProperties}>
|
||||
{features.map((feature, index) => {
|
||||
const row = Math.floor(index / columns);
|
||||
const labelPosition = rowCount >= 2 ? (row === 0 ? 'above' : 'below') : 'above';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={feature.key}
|
||||
className={cx('dt-feature-legend-item', stateClassMap[feature.state])}
|
||||
style={{ width: `${100 / columns}%` }}
|
||||
onMouseEnter={() => setHoveredFeature(feature.key)}
|
||||
onMouseLeave={() => setHoveredFeature(null)}>
|
||||
|
||||
{/* Rotated vertical label — always in DOM, visibility via CSS */}
|
||||
<div className={cx(
|
||||
'dt-feature-legend-rotated-label',
|
||||
labelPosition === 'below' ? 'label-below' : 'label-above',
|
||||
showLabels && 'label-visible',
|
||||
)}>
|
||||
<span>{feature.name}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="dt-feature-legend-icon"
|
||||
style={{ width: `${iconSize}px`, height: `${iconSize}px` }}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* DTRadioGroup — Hexagonal radio buttons.
|
||||
*
|
||||
* CSS reference: forms-dt.css input[type="radio"], .dt-radio-option
|
||||
* Uses a custom span for the hexagon indicator since ::before pseudo-elements
|
||||
* on <input> are unreliable in Chromium/Electron.
|
||||
*
|
||||
* CSS reference: forms-dt.css .dt-radio-hex, .dt-radio-option
|
||||
*/
|
||||
|
||||
import { useId, type CSSProperties } from 'react';
|
||||
@@ -39,20 +42,27 @@ export function DTRadioGroup({
|
||||
className={cx('dt-radio-group', className)}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '4px', ...style }}
|
||||
role="radiogroup">
|
||||
{options.map(option => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={cx('dt-radio-option', value === option.value && 'selected')}>
|
||||
<input
|
||||
type="radio"
|
||||
name={groupName}
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
onChange={() => onChange(option.value)}
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
{options.map(option => {
|
||||
const checked = value === option.value;
|
||||
return (
|
||||
<label
|
||||
key={option.value}
|
||||
className={cx('dt-radio-option', checked && 'selected')}>
|
||||
<input
|
||||
type="radio"
|
||||
name={groupName}
|
||||
value={option.value}
|
||||
checked={checked}
|
||||
onChange={() => onChange(option.value)}
|
||||
style={{ position: 'absolute', opacity: 0, width: 0, height: 0 }}
|
||||
/>
|
||||
<span className={cx('dt-radio-hex', checked && 'checked')}>
|
||||
<span className="dt-radio-hex-inner" />
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user