819 lines
36 KiB
JavaScript
819 lines
36 KiB
JavaScript
// Rôle : expose en développement un catalogue vivant et interactif des contrôles NDS.
|
|
import { useState } from "react";
|
|
import { Icon } from "../components/Icon.jsx";
|
|
import { MixTagsInput } from "../components/MixTagsInput.jsx";
|
|
|
|
const SIZE_VALUES = {
|
|
field: ["xs", "sm", "md"],
|
|
button: ["xs", "sm", "md"],
|
|
iconButton: ["micro", "xxs", "xs", "sm", "md"],
|
|
group: ["xs", "sm", "md"],
|
|
switch: ["xs", "sm", "md", "lg"],
|
|
none: ["default"]
|
|
};
|
|
|
|
const VARIANT_VALUES = {
|
|
button: ["secondary", "primary", "ghost", "dashed", "danger"],
|
|
iconButton: ["secondary", "primary", "ghost", "panel-toggle", "danger"],
|
|
selectTrigger: ["default", "icon-picker"],
|
|
textarea: ["default", "ghost"],
|
|
swatch: ["color", "rainbow"],
|
|
event: ["default", "dark"],
|
|
surface: ["default", "interactive", "tool", "filter", "modal"],
|
|
badge: ["primary", "neutral", "gold", "success", "warning", "danger"],
|
|
feedback: ["info", "success", "warning", "danger"],
|
|
position: ["left", "right"],
|
|
none: ["default"]
|
|
};
|
|
|
|
const EDGE_VALUES = ["none", "left", "middle", "right", "top", "center", "bottom", "top-left", "top-right", "bottom-right", "bottom-left"];
|
|
const BORDER_VALUES = ["default", "joined", "none"];
|
|
const SCROLL_AXIS_VALUES = ["horizontal", "vertical"];
|
|
const EVENT_COLOR_VALUES = ["#8b5cf6", "#f6c453", "#22d3ee", "#d946ef", "#22c55e", "#7d879f"];
|
|
const STATE_VALUES = ["normal", "active", "disabled"];
|
|
const FIELD_STATE_VALUES = ["normal", "disabled"];
|
|
const DEFAULT_STATE_VALUES = ["normal"];
|
|
const CONTENT_VALUES = ["text", "icon", "mixed"];
|
|
|
|
const MIX_TAG_TOKENS = [
|
|
{ value: "base", label: "Base", title: "Valeur base" },
|
|
{ value: "bonus", label: "Bonus", title: "Valeur bonus" }
|
|
];
|
|
|
|
const COLOR_TOKENS = [
|
|
["Page", "var(--color-bg-page)"],
|
|
["Surface", "var(--color-bg-surface)"],
|
|
["Primary", "var(--color-primary)"],
|
|
["Gold", "var(--color-accent-gold)"],
|
|
["Cyan", "var(--color-accent-cyan)"],
|
|
["Danger", "var(--color-danger)"]
|
|
];
|
|
|
|
const RADIO_OPTIONS = {
|
|
text: [
|
|
{ value: "a", label: "A" },
|
|
{ value: "b", label: "B" },
|
|
{ value: "c", label: "C" }
|
|
],
|
|
icon: [
|
|
{ value: "rows", label: "Rows", icon: "rows" },
|
|
{ value: "columns", label: "Columns", icon: "columns" }
|
|
],
|
|
mixed: [
|
|
{ value: "one", label: "One", icon: "eye-open" },
|
|
{ value: "two", label: "Two", icon: "home" },
|
|
{ value: "three", label: "Three", icon: "close" }
|
|
]
|
|
};
|
|
|
|
const COMPONENTS = [
|
|
{
|
|
id: "nds-input",
|
|
name: "Input",
|
|
className: "nds-input",
|
|
sizes: SIZE_VALUES.field,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
|
|
render: ({ attributes, state, label, size }) => <input className="nds-input" {...attributes} type="text" defaultValue={state === "disabled" ? "Disabled" : size.toUpperCase()} disabled={state === "disabled"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-select",
|
|
name: "Select",
|
|
className: "nds-select",
|
|
sizes: SIZE_VALUES.field,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
|
|
render: ({ attributes, state, label }) => (
|
|
<select className="nds-select" {...attributes} defaultValue="b" disabled={state === "disabled"} aria-label={label}>
|
|
<option value="a">Option A</option>
|
|
<option value="b">{state === "disabled" ? "Disabled" : "Option B"}</option>
|
|
<option value="c">Option C</option>
|
|
</select>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-select-trigger",
|
|
name: "Select Trigger",
|
|
className: "nds-select-trigger",
|
|
sizes: SIZE_VALUES.field,
|
|
variants: VARIANT_VALUES.selectTrigger,
|
|
attributes: { "data-size": SIZE_VALUES.field, "data-variant": VARIANT_VALUES.selectTrigger, "data-state": STATE_VALUES },
|
|
render: ({ attributes, state, variant }) => (
|
|
<button className="nds-select-trigger" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>
|
|
<Icon name={variant === "icon-picker" ? "shield" : "calendar"} />
|
|
{variant !== "icon-picker" && <span>{state === "disabled" ? "Disabled" : "27 août"}</span>}
|
|
</button>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-time",
|
|
name: "Time Input",
|
|
className: "nds-time",
|
|
sizes: SIZE_VALUES.field,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
|
|
render: ({ attributes, state, label }) => <input className="nds-time" {...attributes} type="time" defaultValue="21:30" disabled={state === "disabled"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-textarea",
|
|
name: "Textarea",
|
|
className: "nds-textarea",
|
|
sizes: SIZE_VALUES.field,
|
|
variants: VARIANT_VALUES.textarea,
|
|
attributes: { "data-size": SIZE_VALUES.field, "data-variant": VARIANT_VALUES.textarea, "data-state": FIELD_STATE_VALUES },
|
|
render: ({ attributes, state, variant, label }) => <textarea className="nds-textarea" {...attributes} defaultValue={state === "disabled" ? "Disabled" : `${variant || "default"}\nscrollbar commune\nresize`} disabled={state === "disabled"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-inline-input",
|
|
name: "Inline Input",
|
|
className: "nds-inline-input",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-state": FIELD_STATE_VALUES },
|
|
render: ({ state, label }) => <input className="nds-inline-input" type="text" defaultValue={state === "disabled" ? "Disabled" : "Inline edit"} disabled={state === "disabled"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-button",
|
|
name: "Button",
|
|
className: "nds-button",
|
|
sizes: SIZE_VALUES.button,
|
|
variants: VARIANT_VALUES.button,
|
|
configurable: { "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES },
|
|
attributes: { "data-size": SIZE_VALUES.button, "data-variant": VARIANT_VALUES.button, "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES, "data-state": STATE_VALUES },
|
|
render: ({ attributes, state, variant, label }) => <button className="nds-button" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>{variant === "secondary" ? "secondary" : variant || label}</button>
|
|
},
|
|
{
|
|
id: "nds-icon-button",
|
|
name: "Icon Button",
|
|
className: "nds-icon-button",
|
|
sizes: SIZE_VALUES.iconButton,
|
|
variants: VARIANT_VALUES.iconButton,
|
|
configurable: { "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES },
|
|
attributes: { "data-size": SIZE_VALUES.iconButton, "data-variant": VARIANT_VALUES.iconButton, "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES, "data-state": STATE_VALUES },
|
|
render: ({ attributes, state, variant, label }) => {
|
|
const icon = variant === "danger" ? "trash" : variant === "primary" ? "export" : "settings";
|
|
return <button className="nds-icon-button" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label}><Icon name={icon} /></button>;
|
|
}
|
|
},
|
|
{
|
|
id: "nds-radio-button-group",
|
|
name: "Radio Button Group",
|
|
className: "nds-radio-button-group",
|
|
sizes: SIZE_VALUES.group,
|
|
variants: VARIANT_VALUES.none,
|
|
configurable: { content: CONTENT_VALUES },
|
|
attributes: { "data-size": SIZE_VALUES.group, content: CONTENT_VALUES, "data-state": ["normal", "disabled"] },
|
|
render: ({ attributes, config, state }) => state === "disabled"
|
|
? <div className="nds-radio-button-group" {...attributes} aria-disabled="true"><span>A</span><span>B</span></div>
|
|
: <InteractiveRadioButtonGroup attributes={attributes} options={RADIO_OPTIONS[config.content || "text"]} iconOnly={config.content === "icon"} />
|
|
},
|
|
{
|
|
id: "nds-checkbox",
|
|
name: "Checkbox",
|
|
className: "nds-checkbox",
|
|
sizes: ["md", "sm", "xs"],
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": ["md", "sm", "xs"], "data-state": STATE_VALUES },
|
|
render: ({ attributes, state, label }) => <input className="nds-checkbox" {...attributes} type="checkbox" defaultChecked={state === "active"} disabled={state === "disabled"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-quantity-stepper",
|
|
name: "Quantity Stepper",
|
|
className: "nds-quantity-stepper",
|
|
sizes: SIZE_VALUES.group,
|
|
variants: VARIANT_VALUES.none,
|
|
configurable: { content: ["value", "input"] },
|
|
attributes: { "data-size": SIZE_VALUES.group, content: ["value", "input"] },
|
|
render: ({ attributes, config }) => <InteractiveStepper attributes={attributes} input={config.content === "input"} />
|
|
},
|
|
{
|
|
id: "nds-switch",
|
|
name: "Switch",
|
|
className: "nds-switch",
|
|
sizes: SIZE_VALUES.switch,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": SIZE_VALUES.switch, "data-state": STATE_VALUES },
|
|
render: ({ attributes, state }) => state === "disabled"
|
|
? <button className="nds-switch" {...attributes} type="button" disabled aria-pressed="true"><Icon name="scrollable" /><i aria-hidden="true" /></button>
|
|
: <InteractiveSwitch attributes={attributes} initialActive={state === "active"} />
|
|
},
|
|
{
|
|
id: "nds-native-switch",
|
|
name: "Native Switch",
|
|
className: "nds-native-switch",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-state": STATE_VALUES },
|
|
render: ({ state }) => <NativeSwitchPreview disabled={state === "disabled"} initialChecked={state === "active"} />
|
|
},
|
|
{
|
|
id: "nds-swatch",
|
|
name: "Swatch",
|
|
className: "nds-swatch",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.swatch,
|
|
attributes: { "data-variant": VARIANT_VALUES.swatch, "data-state": STATE_VALUES },
|
|
render: ({ attributes, state, variant, label }) => <button className="nds-swatch" {...attributes} type="button" style={variant === "rainbow" ? undefined : { "--swatch-color": "#22d3ee" }} disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label} />
|
|
},
|
|
{
|
|
id: "nds-compound-toggle-field",
|
|
name: "Compound Toggle Field",
|
|
className: "nds-compound-toggle-field",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-state": STATE_VALUES },
|
|
render: ({ state }) => <div className="nds-compound-toggle-field" aria-disabled={state === "disabled"}><button type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>Hold</button><input className="nds-compound-toggle-field-input" type="number" defaultValue="2" disabled={state === "disabled"} aria-label="Hold seconds" /></div>
|
|
},
|
|
{
|
|
id: "nds-list-row",
|
|
name: "List Row",
|
|
className: "nds-list-row",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-state": ["normal", "active", "disabled"] },
|
|
render: ({ state }) => <div className={`nds-list-row ${state === "active" ? "is-active" : ""} ${state === "disabled" ? "is-disabled" : ""}`.trim()}><button type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>{state === "active" ? "Page active" : "Page"}</button></div>
|
|
},
|
|
{
|
|
id: "nds-large-icon-picker-trigger",
|
|
name: "Large Icon Picker Trigger",
|
|
className: "nds-large-icon-picker-trigger",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-state": STATE_VALUES },
|
|
render: ({ state, label }) => <button className="nds-large-icon-picker-trigger" type="button" disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label}><Icon name="toolbox" /></button>
|
|
},
|
|
{
|
|
id: "nds-event-surface",
|
|
name: "Event Surface",
|
|
className: "nds-event-surface",
|
|
sizes: ["xs", "sm", "md"],
|
|
variants: VARIANT_VALUES.event,
|
|
configurable: { "data-color": EVENT_COLOR_VALUES },
|
|
attributes: { "data-size": ["xs", "sm", "md"], "data-variant": VARIANT_VALUES.event, "data-color": EVENT_COLOR_VALUES },
|
|
render: ({ attributes, config, variant, size }) => (
|
|
<div className="nds-event-surface" {...attributes} style={{ "--nds-event-color": config["data-color"], height: size === "xs" ? "10px" : size === "sm" ? "22px" : "44px" }}>
|
|
<button type="button"><strong>{variant === "dark" ? "Market" : "Raid"}</strong><span>21:30 - 22:45</span></button>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-scrollbox",
|
|
name: "Scrollbox",
|
|
className: "nds-scrollbox",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
configurable: { "data-axis": SCROLL_AXIS_VALUES },
|
|
attributes: { "data-axis": SCROLL_AXIS_VALUES },
|
|
render: ({ attributes, config }) => config["data-axis"] === "horizontal"
|
|
? <div className="nds-scrollbox" {...attributes}><div className="nds-scrollbox-content-wide"><span>Alpha sector</span><span>Beta sector</span><span>Gamma sector</span><span>Delta sector</span><span>Epsilon sector</span><span>Zeta sector</span><span>Eta sector</span></div></div>
|
|
: <div className="nds-scrollbox" {...attributes}><p>Zone scrollable</p><p>Track transparent</p><p>Thumb commun</p><p>Contenu supplémentaire</p><p>Encore une ligne</p></div>
|
|
}
|
|
];
|
|
|
|
const PATTERN_COMPONENTS = [
|
|
{
|
|
id: "nds-panel",
|
|
name: "Panel",
|
|
className: "nds-panel",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: ["default", "interactive"],
|
|
attributes: { "data-variant": ["default", "interactive"] },
|
|
render: ({ attributes, variant }) => (
|
|
<article className="nds-panel design-system-pattern-card" {...attributes}>
|
|
<p className="eyebrow">{variant}</p>
|
|
<strong>Surface de lecture</strong>
|
|
<span>Fond sombre, bordure Nebula et accent dore contenu.</span>
|
|
</article>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-card",
|
|
name: "Card",
|
|
className: "nds-card",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: ["default", "interactive"],
|
|
attributes: { "data-variant": ["default", "interactive"] },
|
|
render: ({ attributes }) => (
|
|
<article className="nds-card design-system-pattern-card" {...attributes}>
|
|
<div className="nds-card-cover design-system-pattern-cover"><Icon name="toolbox" /></div>
|
|
<div className="nds-card-body">
|
|
<p className="eyebrow">Card</p>
|
|
<strong>Element repete</strong>
|
|
<div className="nds-card-actions"><button className="nds-button" data-size="xs" type="button">Ouvrir</button></div>
|
|
</div>
|
|
</article>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-tool-panel",
|
|
name: "Tool Panel",
|
|
className: "nds-tool-panel",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: ["tool", "interactive"],
|
|
attributes: { "data-variant": ["tool", "interactive"] },
|
|
render: ({ attributes }) => (
|
|
<section className="nds-tool-panel design-system-pattern-card" {...attributes}>
|
|
<header className="nds-section-heading"><strong>Outil</strong><button className="nds-icon-button" data-size="xs" type="button" aria-label="Ajouter"><Icon name="add" /></button></header>
|
|
<p>Surface de travail compacte pour les toolboxes.</p>
|
|
</section>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-toolbar",
|
|
name: "Toolbar",
|
|
className: "nds-toolbar",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: {},
|
|
render: () => (
|
|
<div className="nds-toolbar design-system-pattern-inline">
|
|
<button className="nds-button" data-size="xs" data-variant="primary" type="button">Ajouter</button>
|
|
<button className="nds-icon-button" data-size="xs" type="button" aria-label="Parametres"><Icon name="settings" /></button>
|
|
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" aria-label="Supprimer"><Icon name="trash" /></button>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-section-heading",
|
|
name: "Section Heading",
|
|
className: "nds-section-heading",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: {},
|
|
render: () => (
|
|
<header className="nds-section-heading design-system-pattern-wide">
|
|
<div><p className="eyebrow">Heading</p><strong>Section</strong></div>
|
|
<button className="nds-button" data-size="xs" type="button">Action</button>
|
|
</header>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-page-hero",
|
|
name: "Page Hero",
|
|
className: "nds-page-hero",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: {},
|
|
render: () => (
|
|
<section className="nds-page-hero design-system-pattern-hero">
|
|
<div><p className="eyebrow">Hero</p><strong>Page concise</strong><span>Introduction courte et lisible.</span></div>
|
|
</section>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-badge",
|
|
name: "Badge",
|
|
className: "nds-badge",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.badge,
|
|
attributes: { "data-variant": VARIANT_VALUES.badge },
|
|
render: ({ attributes, variant }) => <span className="nds-badge" {...attributes}>{variant}</span>
|
|
},
|
|
{
|
|
id: "nds-chip",
|
|
name: "Chip",
|
|
className: "nds-chip",
|
|
sizes: ["sm", "default"],
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: { "data-size": ["sm", "default"], "data-state": STATE_VALUES },
|
|
render: ({ attributes, state }) => <button className="nds-chip" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"}><Icon name="checklist" />Filtre</button>
|
|
},
|
|
{
|
|
id: "nds-empty-state",
|
|
name: "Empty State",
|
|
className: "nds-empty-state",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: {},
|
|
render: () => <div className="nds-empty-state design-system-pattern-card"><strong>Aucun contenu</strong><span>Message court et action optionnelle.</span></div>
|
|
},
|
|
{
|
|
id: "nds-inline-notice",
|
|
name: "Inline Notice",
|
|
className: "nds-inline-notice",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.feedback,
|
|
attributes: { "data-variant": VARIANT_VALUES.feedback },
|
|
render: ({ attributes, variant }) => <p className="nds-inline-notice" {...attributes}>{variant === "info" ? "Information compacte" : `${variant} compact`}</p>
|
|
},
|
|
{
|
|
id: "nds-progress",
|
|
name: "Progress",
|
|
className: "nds-progress",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: ["primary", "success", "warning", "danger"],
|
|
attributes: { "data-variant": ["primary", "success", "warning", "danger"] },
|
|
render: ({ attributes, variant }) => <span className="nds-progress" {...attributes} aria-label={`Progression ${variant}`}><span style={{ width: variant === "danger" ? "86%" : "58%" }} /></span>
|
|
},
|
|
{
|
|
id: "nds-filter-panel",
|
|
name: "Filter Panel",
|
|
className: "nds-filter-panel",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
attributes: {},
|
|
render: () => (
|
|
<aside className="nds-filter-panel design-system-pattern-card">
|
|
<header className="nds-section-heading"><strong>Filtres</strong><button className="nds-icon-button" data-size="xs" type="button" aria-label="Reset"><Icon name="rubber" /></button></header>
|
|
<button className="nds-chip" data-size="sm" type="button" aria-pressed="true">Actif</button>
|
|
</aside>
|
|
)
|
|
},
|
|
{
|
|
id: "nds-reorder-indicator",
|
|
name: "Reorder Indicator",
|
|
className: "nds-reorder-indicator",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: VARIANT_VALUES.none,
|
|
configurable: { "data-axis": ["vertical", "horizontal"], "data-placement": ["before", "after"] },
|
|
attributes: { "data-axis": ["vertical", "horizontal"], "data-placement": ["before", "after"] },
|
|
render: ({ attributes }) => <div className="nds-reorder-indicator design-system-pattern-reorder" {...attributes}>Drop target</div>
|
|
},
|
|
{
|
|
id: "nds-overlays",
|
|
name: "Overlay Patterns",
|
|
className: "nds-modal / nds-drawer / nds-toast / nds-popover",
|
|
sizes: SIZE_VALUES.none,
|
|
variants: ["modal", "drawer", "toast", "popover"],
|
|
attributes: { "data-variant": ["modal", "drawer", "toast", "popover"] },
|
|
render: ({ variant }) => {
|
|
if (variant === "drawer") return <div className="nds-drawer-panel design-system-pattern-overlay">Drawer panel</div>;
|
|
if (variant === "toast") return <div className="nds-toast design-system-pattern-toast"><span>Alerte globale</span><button className="nds-icon-button" data-size="xs" type="button" aria-label="Fermer"><Icon name="close" /></button></div>;
|
|
if (variant === "popover") return <div className="nds-popover design-system-pattern-overlay">Menu flottant</div>;
|
|
return <div className="nds-modal design-system-pattern-overlay"><strong>Modale</strong><footer className="nds-modal-footer"><button className="nds-button" data-size="xs" type="button">Annuler</button></footer></div>;
|
|
}
|
|
}
|
|
];
|
|
|
|
const NAV_ITEMS = [
|
|
{ id: "foundations", label: "Foundations" },
|
|
...COMPONENTS.map(({ id, name }) => ({ id, label: name })),
|
|
...PATTERN_COMPONENTS.map(({ id, name }) => ({ id, label: name })),
|
|
{ id: "date-picker-buttons", label: "Date Picker Buttons" },
|
|
{ id: "specialized-components", label: "Specialized Components" },
|
|
{ id: "audit", label: "Audit" }
|
|
];
|
|
|
|
function dataProps(attributes = {}) {
|
|
return Object.fromEntries(Object.entries(attributes).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
}
|
|
|
|
function attributeLabel(attributes = {}) {
|
|
const entries = Object.entries(attributes).filter(([, value]) => value && value !== "default" && value !== "secondary" && value !== "md" && value !== "vertical" && value !== "none" && value !== "normal");
|
|
return entries.length ? entries.map(([key, value]) => `${key}="${value}"`).join(" ") : "default";
|
|
}
|
|
|
|
function DesignSystemNav() {
|
|
const scrollToSection = (id) => {
|
|
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
};
|
|
|
|
return (
|
|
<aside className="design-system-nav" aria-label="Navigation design system">
|
|
<div>
|
|
<p className="eyebrow">Sokko G</p>
|
|
<h1>Nebula Design System</h1>
|
|
<p>Catalogue vivant des classes publiques, attributs, tailles, variants et états des contrôles interactifs Nebula.</p>
|
|
</div>
|
|
<div className="design-system-token-stack">
|
|
<SpecPill label="Theme" value="Nebula Command" />
|
|
<SpecPill label="Focus" value="gold outline" />
|
|
<SpecPill label="API" value="class + data-*" />
|
|
</div>
|
|
<nav>
|
|
{NAV_ITEMS.map((item) => <button key={item.id} type="button" onClick={() => scrollToSection(item.id)}>{item.label}</button>)}
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function SpecPill({ label, value }) {
|
|
return <span className="design-system-spec-pill"><b>{label}</b><span>{value}</span></span>;
|
|
}
|
|
|
|
function ComponentSection({ id, title, meta, children }) {
|
|
return (
|
|
<section id={id} className="design-system-section">
|
|
<header>
|
|
<div>
|
|
<p className="eyebrow">{meta}</p>
|
|
<h2>{title}</h2>
|
|
</div>
|
|
</header>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function AttributeSummary({ component }) {
|
|
return (
|
|
<article className="design-system-component-intro">
|
|
<div className="design-system-doc-summary">
|
|
<span>Element</span>
|
|
<strong>{component.name}</strong>
|
|
<span>Classe</span>
|
|
<code>{component.className}</code>
|
|
</div>
|
|
<div className="design-system-attributes-doc">
|
|
<span>Attributes</span>
|
|
<dl>
|
|
{Object.entries(component.attributes).map(([name, values]) => (
|
|
<div key={name}>
|
|
<dt><code>{name}</code></dt>
|
|
<dd>{values.map((value) => <code key={value}>{value}</code>)}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function VariantMatrix({ columns, rows, controls, children }) {
|
|
return (
|
|
<article className="design-system-matrix">
|
|
{controls && <header>{controls}</header>}
|
|
<div className="design-system-matrix-grid" style={{ "--nds-matrix-columns": columns.length }}>
|
|
<div className="design-system-matrix-corner" aria-hidden="true" />
|
|
{columns.map((column) => <div key={column} className="design-system-matrix-heading">{column}</div>)}
|
|
{children || rows.map((row) => <StatePreview key={row.label} label={row.label} cells={row.cells} />)}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function StatePreview({ label, cells }) {
|
|
const [attribute, value] = label.includes("=") ? label.split("=") : [null, label];
|
|
return (
|
|
<>
|
|
<div className="design-system-matrix-row-label">
|
|
{attribute ? (
|
|
<>
|
|
<span>{attribute}</span>
|
|
<strong>{value}</strong>
|
|
</>
|
|
) : <strong>{value}</strong>}
|
|
</div>
|
|
{cells.map((cell, index) => <div key={`${label}-${index}`} className="design-system-matrix-cell">{cell}</div>)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ConfigSelect({ name, values, value, onChange }) {
|
|
return (
|
|
<label className="design-system-config-field">
|
|
<span>{name}</span>
|
|
<select className="nds-select" data-size="xs" value={value} onChange={(event) => onChange(name, event.target.value)}>
|
|
{values.map((option) => <option key={option} value={option}>{option}</option>)}
|
|
</select>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function ControlSample({ children }) {
|
|
return (
|
|
<div className="design-system-control-sample">
|
|
<div className="design-system-sample-preview">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ComponentCatalogSection({ component }) {
|
|
const [config, setConfig] = useState(() => Object.fromEntries(Object.entries(component.configurable || {}).map(([name, values]) => [name, values[0]])));
|
|
const sizes = component.sizes;
|
|
const variants = component.variants;
|
|
const stateValues = component.attributes["data-state"] || DEFAULT_STATE_VALUES;
|
|
const stateRows = stateValues.filter((state) => state !== "normal");
|
|
const hasConfigurableAttributes = Boolean(component.configurable && Object.keys(component.configurable).length);
|
|
const defaultStateVariant = variants.includes("secondary") ? "secondary" : variants[0];
|
|
const setConfigValue = (name, value) => setConfig((current) => ({ ...current, [name]: value }));
|
|
const renderCell = ({ size, variant, state = "normal" }) => {
|
|
const attrs = {
|
|
...Object.fromEntries(Object.entries(config).filter(([name]) => name.startsWith("data-"))),
|
|
"data-size": size,
|
|
"data-variant": variant,
|
|
"data-edge": config["data-edge"],
|
|
"data-border": config["data-border"],
|
|
"data-axis": config["data-axis"],
|
|
"data-state": state
|
|
};
|
|
const attributes = dataProps(attrs);
|
|
const label = `${component.className} ${attributeLabel(attributes)}`;
|
|
return <ControlSample key={`${variant}-${size}-${state}`} label={label}>{component.render({ attributes, config, state, variant, label, size })}</ControlSample>;
|
|
};
|
|
|
|
return (
|
|
<ComponentSection id={component.id} title={component.name} meta="NDS element">
|
|
<AttributeSummary component={component} />
|
|
<VariantMatrix
|
|
title="data-size x data-variant"
|
|
note="Les colonnes pilotent data-size. Les lignes affichent d'abord data-variant, puis data-state. Le formulaire applique les autres attributs à toute la matrice."
|
|
columns={sizes.map((size) => size === "default" ? "default" : `data-size=${size}`)}
|
|
controls={hasConfigurableAttributes && (
|
|
<div className="design-system-config-panel">
|
|
{Object.entries(component.configurable).map(([name, values]) => <ConfigSelect key={name} name={name} values={values} value={config[name]} onChange={setConfigValue} />)}
|
|
</div>
|
|
)}
|
|
>
|
|
{variants.map((variant) => (
|
|
<StatePreview
|
|
key={variant}
|
|
label={variant === "default" ? "default" : `data-variant=${variant}`}
|
|
cells={sizes.map((size) => renderCell({ size, variant }))}
|
|
/>
|
|
))}
|
|
{stateRows.length > 0 && <div className="design-system-matrix-separator">data-state</div>}
|
|
{stateRows.map((state) => (
|
|
<StatePreview
|
|
key={state}
|
|
label={`data-state=${state}`}
|
|
cells={sizes.map((size) => renderCell({ size, variant: defaultStateVariant, state }))}
|
|
/>
|
|
))}
|
|
</VariantMatrix>
|
|
</ComponentSection>
|
|
);
|
|
}
|
|
|
|
function ToggleButton({ children, className = "nds-button", attributes, ariaLabel }) {
|
|
const [pressed, setPressed] = useState(false);
|
|
return (
|
|
<button className={className} {...dataProps(attributes)} type="button" aria-pressed={pressed} aria-label={ariaLabel} onClick={() => setPressed((value) => !value)}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function InteractiveRadioButtonGroup({ attributes, options, iconOnly = false }) {
|
|
const [value, setValue] = useState(options[0]?.value);
|
|
return (
|
|
<div className="nds-radio-button-group" {...dataProps(attributes)} role="radiogroup" aria-label="Groupe interactif">
|
|
{options.map((option) => {
|
|
const active = option.value === value;
|
|
return (
|
|
<button key={option.value} className={active ? "is-active active" : ""} type="button" role="radio" aria-checked={active} aria-label={iconOnly ? option.label : undefined} onClick={() => setValue(option.value)}>
|
|
{option.icon && <Icon name={option.icon} />}
|
|
{!iconOnly && <span>{option.label}</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InteractiveStepper({ attributes, input = false }) {
|
|
const [value, setValue] = useState(2);
|
|
const increment = (step) => setValue((current) => Math.min(20, Math.max(0, current + step)));
|
|
return (
|
|
<span className="nds-quantity-stepper" {...dataProps(attributes)} aria-label="Stepper interactif">
|
|
<button type="button" aria-label="Retirer" onClick={() => increment(-1)}><span aria-hidden="true">-</span></button>
|
|
<span className="nds-quantity-stepper-value">
|
|
{input ? <input className="nds-quantity-stepper-input" type="number" min="0" max="20" value={value} onChange={(event) => setValue(Math.min(20, Math.max(0, Number.parseInt(event.target.value, 10) || 0)))} aria-label="Quantité actuelle" /> : value}
|
|
<span>/ 20</span>
|
|
</span>
|
|
<button type="button" aria-label="Ajouter" onClick={() => increment(1)}><Icon name="add" /></button>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function InteractiveSwitch({ attributes, initialActive = false }) {
|
|
const [active, setActive] = useState(initialActive);
|
|
return (
|
|
<button className="nds-switch" {...dataProps(attributes)} type="button" aria-pressed={active} onClick={() => setActive((value) => !value)}>
|
|
<Icon name="scrollable" />
|
|
<i aria-hidden="true" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function NativeSwitchPreview({ disabled = false, initialChecked = false }) {
|
|
const [checked, setChecked] = useState(initialChecked);
|
|
return (
|
|
<label className="nds-native-switch">
|
|
<input type="checkbox" checked={checked} disabled={disabled} onChange={(event) => setChecked(event.target.checked)} aria-label="Inclure" />
|
|
<span aria-hidden="true" />
|
|
<b>{disabled ? "Disabled" : "Inclus"}</b>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function DatePickerPreview() {
|
|
const [activeDay, setActiveDay] = useState(2);
|
|
const days = Array.from({ length: 35 }, (_, index) => {
|
|
const day = index - 2;
|
|
return { id: index, label: day > 0 && day <= 31 ? day : "", outside: day <= 0 || day > 31 };
|
|
});
|
|
return (
|
|
<div className="nds-date-picker-preview" role="group" aria-label="Mini calendrier interactif">
|
|
{["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => <span key={day} className="nds-date-picker-preview-day">{day}</span>)}
|
|
{days.map((day) => (
|
|
<button key={day.id} className={`${day.label === activeDay ? "active" : ""} ${day.outside ? "is-outside" : ""}`.trim()} type="button" aria-pressed={day.label === activeDay} disabled={day.outside} onClick={() => setActiveDay(day.label)}>
|
|
{day.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FoundationsSection() {
|
|
return (
|
|
<ComponentSection id="foundations" title="Foundations" meta="tokens">
|
|
<div className="design-system-foundations">
|
|
<article className="design-system-type-card"><p>Typography</p><strong>Aa</strong><span>Inter, system-ui</span></article>
|
|
<article className="design-system-color-card">
|
|
<p>Colors</p>
|
|
<div className="design-system-swatches">{COLOR_TOKENS.map(([label, color]) => <span key={label} style={{ "--nds-swatch-color": color }} title={label} />)}</div>
|
|
</article>
|
|
<article className="design-system-radius-card">
|
|
<p>Radius</p>
|
|
<div><span style={{ borderRadius: "var(--radius-sm)" }}>sm</span><span style={{ borderRadius: "var(--radius-md)" }}>md</span><span style={{ borderRadius: "var(--radius-lg)" }}>lg</span></div>
|
|
</article>
|
|
<article className="design-system-focus-card"><p>Focus</p><button className="nds-button" data-variant="primary" type="button">Focus visible</button></article>
|
|
</div>
|
|
</ComponentSection>
|
|
);
|
|
}
|
|
|
|
function DatePickerButtonsSection() {
|
|
return (
|
|
<ComponentSection id="date-picker-buttons" title="Date Picker Buttons" meta="internal NDS pattern">
|
|
<VariantMatrix title="month grid" note="Boutons internes du mini calendrier, affichés avec un mois complet." columns={["interactive"]} rows={[{ label: "buttons", cells: [<ControlSample label="date-picker-button mixin"><DatePickerPreview /></ControlSample>] }]} />
|
|
</ComponentSection>
|
|
);
|
|
}
|
|
|
|
function SpecializedComponentsSection() {
|
|
const [mixValue, setMixValue] = useState("base + bonus");
|
|
const [smallMixValue, setSmallMixValue] = useState("base * 2");
|
|
|
|
return (
|
|
<ComponentSection id="specialized-components" title="Specialized Components" meta="not NDS public classes">
|
|
<VariantMatrix
|
|
title="Composants spécialisés conservés"
|
|
note="Ces éléments restent spécialisés : ils ne créent pas de classe de famille NDS publique."
|
|
columns={["MixTagsInput", "MixTagsInput small"]}
|
|
rows={[{
|
|
label: "interactive",
|
|
cells: [
|
|
<ControlSample label="MixTagsInput"><MixTagsInput value={mixValue} onChange={setMixValue} tokens={MIX_TAG_TOKENS} insertToken={{ value: "base", icon: "add", title: "Insérer base" }} aria-label="MixTagsInput standard" /></ControlSample>,
|
|
<ControlSample label="MixTagsInput size=small"><MixTagsInput size="small" value={smallMixValue} onChange={setSmallMixValue} tokens={MIX_TAG_TOKENS} insertToken={{ value: "bonus", icon: "add", title: "Insérer bonus" }} aria-label="MixTagsInput compact" /></ControlSample>
|
|
]
|
|
}]}
|
|
/>
|
|
</ComponentSection>
|
|
);
|
|
}
|
|
|
|
function AuditSection() {
|
|
return (
|
|
<ComponentSection id="audit" title="Audit" meta="api conventions">
|
|
<VariantMatrix
|
|
title="NDS Attribute API"
|
|
note="Une classe publique correspond à une famille. Les attributs data-* configurent les variations. Les états restent natifs, ARIA ou métier."
|
|
columns={["family class", "configuration", "state"]}
|
|
rows={[
|
|
{
|
|
label: "rule",
|
|
cells: [
|
|
<code>className="nds-button"</code>,
|
|
<code>data-size data-variant data-edge data-border data-axis</code>,
|
|
<code>disabled aria-pressed .active .is-active</code>
|
|
]
|
|
},
|
|
{
|
|
label: "global",
|
|
cells: [
|
|
<code>Home / About / Games CTAs</code>,
|
|
<code>Shell / drawer / modales / filters</code>,
|
|
<code>Image viewer / library actions</code>
|
|
]
|
|
},
|
|
{
|
|
label: "sample",
|
|
cells: [
|
|
<button className="nds-icon-button" data-size="sm" data-variant="primary" type="button" aria-label="Exemple"><Icon name="export" /></button>,
|
|
<button className="nds-button" data-size="xs" data-variant="dashed" data-edge="left" data-border="joined" type="button"><Icon name="add" />XS</button>,
|
|
<ToggleButton attributes={{ "data-variant": "primary" }}>Actif</ToggleButton>
|
|
]
|
|
}
|
|
]}
|
|
/>
|
|
</ComponentSection>
|
|
);
|
|
}
|
|
|
|
export function DesignSystemPage() {
|
|
return (
|
|
<div className="design-system-page">
|
|
<DesignSystemNav />
|
|
<main className="design-system-main">
|
|
<FoundationsSection />
|
|
{COMPONENTS.map((component) => <ComponentCatalogSection key={component.id} component={component} />)}
|
|
{PATTERN_COMPONENTS.map((component) => <ComponentCatalogSection key={component.id} component={component} />)}
|
|
<DatePickerButtonsSection />
|
|
<SpecializedComponentsSection />
|
|
<AuditSection />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|