Compare commits

..

2 commits

Author SHA1 Message Date
817e21567b Migrate site controls to Nebula design system
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s
2026-08-29 09:16:58 +02:00
e3d0925eb1 bd init: initialize beads issue tracking 2026-08-27 10:17:31 +02:00
143 changed files with 6093 additions and 2363 deletions

View file

@ -0,0 +1,80 @@
---
name: beads
description: Use when working in a repository that uses bd or Beads for durable project task tracking, issue dependencies, blocker management, multi-session handoff, or shared work memory. Trigger when the user asks to find ready work, claim or close tasks, create follow-up work, inspect blockers, recover project context, or choose between local planning and persistent project tracking.
---
# Beads
Use Beads as the shared project task system. Local plans, scratch files, and personal memories are useful, but they are not the durable source of truth for project work.
## First Step
Run:
```bash
bd prime
```
If that prints nothing, check whether the repository has an active Beads workspace:
```bash
bd where
```
## Preferred Route
Use the `bd` CLI when shell access is available. It is the most compact and direct Beads interface.
## Core CLI Workflow
1. Find work:
```bash
bd ready
bd list --status=open
bd list --status=in_progress
```
2. Inspect before editing:
```bash
bd show <id>
```
3. Claim work atomically:
```bash
bd update <id> --claim
```
4. Create durable follow-up work when implementation reveals new tasks:
```bash
bd create "Short title" --description="Why this exists and what needs to be done" --type=task --priority=2
```
5. Close completed work:
```bash
bd close <id> --reason="Completed"
```
## What Belongs In Beads
Use Beads for:
- shared project tasks
- blockers and dependencies
- discovered follow-up work
- work that must survive thread reset, compaction, or handoff
- status that another person or agent should be able to resume
Use agent-local planning tools only for the current turn's execution checklist. Do not treat them as shared project state.
## Rules
- Do not create markdown TODO files as the source of truth when Beads is available.
- Do not use `bd edit`; it opens an interactive editor. Use `bd update` flags instead.
- Prefer `--json` when parsing `bd` output programmatically.
- If hooks are installed, `bd prime` may already be injected. Run it manually when context is missing.
- Do not auto-close or mutate tasks unless the work is actually complete.

View file

@ -0,0 +1,4 @@
interface:
display_name: "Beads"
short_description: "Project task tracking with bd"
default_prompt: "Use $beads to inspect ready work and manage durable project tasks."

77
.beads/.gitignore vendored Normal file
View file

@ -0,0 +1,77 @@
# Dolt database (managed by Dolt, not git)
dolt/
embeddeddolt/
proxieddb/
# Runtime files
bd.sock
bd.sock.startlock
sync-state.json
last-touched
.exclusive-lock
# Daemon runtime (lock, log, pid)
daemon.*
# Push state (runtime, per-machine)
push-state.json
# Lock files (various runtime locks)
*.lock
# Credential key (encryption key for federation peer auth — never commit)
.beads-credential-key
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
proxied_server_client_info.json
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
export-state/
export-state.json
last_pull
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
ephemeral.sqlite3
ephemeral.sqlite3-journal
ephemeral.sqlite3-wal
ephemeral.sqlite3-shm
# Dolt server management (auto-started by bd)
dolt-server.pid
dolt-server.log
dolt-server.lock
dolt-server.port
dolt-server.activity
# Debug-mode pprof artifacts (written when dolt.debug: true in config.yaml)
dolt-pprof/
# Corrupt backup directories (created by bd doctor --fix recovery)
*.corrupt.backup/
# Backup data (auto-exported JSONL, local-only)
backup/
# Per-project environment file (Dolt connection config, GH#2520)
.env
# Legacy files (from pre-Dolt versions)
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
db.sqlite
bd.db
# NOTE: Do NOT add negation patterns here.
# They would override fork protection in .git/info/exclude.
# Config files (metadata.json, config.yaml) are tracked by git by default
# since no pattern above ignores them.

81
.beads/README.md Normal file
View file

@ -0,0 +1,81 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --claim
bd update <issue-id> --status done
# Sync with Dolt remote
bd dolt push
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in Dolt database with version control and branching
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Sync-ready**: Uses Dolt remotes for backup and team sharing
## Why Beads?
✨ **AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Dolt-native sync via bd dolt push / bd dolt pull
- Branch-aware issue tracking
- Dolt-native three-way merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought* ⚡

68
.beads/config.yaml Normal file
View file

@ -0,0 +1,68 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: JSONL-only, no Dolt database
# When true, .beads/issues.jsonl is the only local store
# no-db: false
# Enable JSON output by default
# json: false
# Feedback title formatting for mutating commands (create/update/close/dep/edit)
# 0 = hide titles, N > 0 = truncate to N characters
# output:
# title-length: 255
# Default actor for audit trails (overridden by BEADS_ACTOR or --actor)
# actor: ""
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
# When enabled, new events are appended incrementally using a high-water mark.
# Use 'bd export --events' to trigger manually regardless of this setting.
# events-export: false
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct database
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Dolt-native backup (periodic backup for off-machine recovery)
# This is full database backup only. Cross-machine sync uses Dolt remotes.
# backup:
# enabled: false # Disable auto-backup entirely
# interval: 15m # Minimum time between auto-backups
# git-push: false # Disable git push (backup locally only)
# git-repo: "" # Separate git repo for backups (default: project repo)
# Optional JSONL auto-export for viewers, interchange, and issue-level migration.
# Disabled by default; enable only when an integration needs fresh .beads/issues.jsonl.
# Use relative paths under .beads/ for JSONL import/export filenames.
# export:
# auto: false
# path: issues.jsonl
# interval: 60s
# git-add: false
# import:
# path: issues.jsonl
# Integration settings (access with 'bd config get/set')
# Non-secret keys (stored in the database):
# - jira.url, jira.project
# - linear.team_id
# - github.org, github.repo
#
# Secret keys (stored in this file but prefer env vars to avoid git exposure):
# - linear.api_key → use LINEAR_API_KEY env var instead
# - github.token → use GITHUB_TOKEN env var instead
sync.remote: "git+ssh://git@git.shinuwa.fr:2222/shinuwa/sokko-g.git"

33
.beads/hooks/post-checkout Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'post-checkout' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run post-checkout "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-checkout'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---

33
.beads/hooks/post-merge Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'post-merge' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run post-merge "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-merge'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---

33
.beads/hooks/pre-commit Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'pre-commit' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run pre-commit "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-commit'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---

33
.beads/hooks/pre-push Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'pre-push' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run pre-push "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-push'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---

33
.beads/hooks/prepare-commit-msg Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'prepare-commit-msg' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---

22
.beads/interactions.jsonl Normal file
View file

@ -0,0 +1,22 @@
{"id":"int-8d5f8f5714677d91360a20177b1f5a9e","kind":"field_change","created_at":"2026-08-27T12:58:35.614943857Z","actor":"Shinuwa","issue_id":"sokko-g-lki","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-206ccb97f9d09712e2b4136d115a0e43","kind":"field_change","created_at":"2026-08-27T13:09:45.211867088Z","actor":"Shinuwa","issue_id":"sokko-g-e7s","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed remaining toolbox control migration to NDS mixins and updated design-system catalogue examples."}}
{"id":"int-5131e4130f57a4e8a55f176a46240fad","kind":"field_change","created_at":"2026-08-27T16:19:44.570963534Z","actor":"Shinuwa","issue_id":"sokko-g-npt","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed NDS button variant cleanup and catalogue simplification."}}
{"id":"int-3db2dfcbabd4cbb63eaeedb1bea8ab47","kind":"field_change","created_at":"2026-08-27T16:43:20.612129607Z","actor":"Shinuwa","issue_id":"sokko-g-0o4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed NDS data attribute API migration and validation."}}
{"id":"int-578a8a6aff782082d539f0e14bb0aa79","kind":"field_change","created_at":"2026-08-27T16:58:07.410812508Z","actor":"Shinuwa","issue_id":"sokko-g-alp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Expanded /design-system into exhaustive NDS family attribute matrices."}}
{"id":"int-cc32f7330c12cf2d37b0d22650ccfb5e","kind":"field_change","created_at":"2026-08-27T17:13:59.871782552Z","actor":"Shinuwa","issue_id":"sokko-g-pp5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reworked /design-system into component cards with configurable attribute matrices."}}
{"id":"int-17da0005e87891c66734a766d8456459","kind":"field_change","created_at":"2026-08-27T17:23:07.045839669Z","actor":"Shinuwa","issue_id":"sokko-g-73i","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-d0531629593c076b63281346c729ac3e","kind":"field_change","created_at":"2026-08-27T17:28:11.740302589Z","actor":"Shinuwa","issue_id":"sokko-g-wnh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-fb41489d0517f51fcda643c4230461fe","kind":"field_change","created_at":"2026-08-27T17:35:40.407737936Z","actor":"Shinuwa","issue_id":"sokko-g-s0a","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-b7bafbc214af080356e07d2d97f1ffba","kind":"field_change","created_at":"2026-08-27T21:32:26.611249333Z","actor":"Shinuwa","issue_id":"sokko-g-2u9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-ea50d03c7c84b32ab2fce86f15bbe05f","kind":"field_change","created_at":"2026-08-27T21:43:00.775108889Z","actor":"Shinuwa","issue_id":"sokko-g-bh4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Completed"}}
{"id":"int-4f7a574335bb2d616e9caae09e22e55b","kind":"field_change","created_at":"2026-08-27T21:45:18.547229481Z","actor":"Shinuwa","issue_id":"sokko-g-7jm","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-ae5fb0bbe41a7958929560a1bbf65052","kind":"field_change","created_at":"2026-08-27T21:48:45.268794089Z","actor":"Shinuwa","issue_id":"sokko-g-z2t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-8e3b38de72d948a6c241b0d8b2f8077b","kind":"field_change","created_at":"2026-08-27T21:54:30.02159376Z","actor":"Shinuwa","issue_id":"sokko-g-coh","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Completed"}}
{"id":"int-0d86b94b9a04df5b68e2362e0824a37b","kind":"field_change","created_at":"2026-08-27T21:57:24.401323676Z","actor":"Shinuwa","issue_id":"sokko-g-hif","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-3fcb6bc2b605db6210c0da18da07e902","kind":"field_change","created_at":"2026-08-27T22:01:35.180268661Z","actor":"Shinuwa","issue_id":"sokko-g-6fs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed NDS textarea min heights by size"}}
{"id":"int-611cb19bdd31f0e79cc8c2f2bb7a3dd7","kind":"field_change","created_at":"2026-08-28T07:13:54.812999765Z","actor":"Shinuwa","issue_id":"sokko-g-tvo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed NDS field hover cascade and replaced public date trigger with select trigger"}}
{"id":"int-0e1196655dabd3096c10beef1ba8aff2","kind":"field_change","created_at":"2026-08-28T07:19:28.131918475Z","actor":"Shinuwa","issue_id":"sokko-g-z4x","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed select trigger hover chevron and focus-visible outline"}}
{"id":"int-9c27343c043121862608b8ed9013c306","kind":"field_change","created_at":"2026-08-28T07:25:53.259127006Z","actor":"Shinuwa","issue_id":"sokko-g-eln","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed select trigger md hover/focus and disabled checkbox styling"}}
{"id":"int-da5413e82c18d19a8432288bec703dd7","kind":"field_change","created_at":"2026-08-28T07:50:07.033524644Z","actor":"Shinuwa","issue_id":"sokko-g-h7z","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed global NDS control migration and validation"}}
{"id":"int-42f39cb889dfab3bc998d4ce566d2339","kind":"field_change","created_at":"2026-08-28T08:09:50.738078383Z","actor":"Shinuwa","issue_id":"sokko-g-9p1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed filter radio group, sidebar storage buttons, memo controls, and textarea ghost variant"}}
{"id":"int-03ed1c823da90551f51a5a0d5691e440","kind":"field_change","created_at":"2026-08-28T12:35:29.257332022Z","actor":"Shinuwa","issue_id":"sokko-g-39h","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Applied NDS event-surface to calendar week and month events, documented data-color/data-size, and validated checks."}}

7
.beads/metadata.json Normal file
View file

@ -0,0 +1,7 @@
{
"database": "dolt",
"backend": "dolt",
"dolt_mode": "embedded",
"dolt_database": "sokko_g",
"project_id": "383fe8d1-bd71-460f-9ff5-e71f14df4827"
}

15
.claude/settings.json Normal file
View file

@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "bd prime --hook-json",
"type": "command"
}
],
"matcher": ""
}
]
}
}

2
.codex/config.toml Normal file
View file

@ -0,0 +1,2 @@
[features]
hooks = true

51
.codex/hooks.json Normal file
View file

@ -0,0 +1,51 @@
{
"hooks": {
"PostCompact": [
{
"hooks": [
{
"command": "bd codex-hook PostCompact",
"statusMessage": "Scheduling Beads context refresh",
"type": "command"
}
],
"matcher": "manual|auto"
}
],
"PreCompact": [
{
"hooks": [
{
"command": "bd codex-hook PreCompact",
"statusMessage": "Checking Beads context",
"type": "command"
}
],
"matcher": "manual|auto"
}
],
"SessionStart": [
{
"hooks": [
{
"command": "bd codex-hook SessionStart",
"statusMessage": "Loading Beads context",
"type": "command"
}
],
"matcher": "startup|resume|clear"
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"command": "bd codex-hook UserPromptSubmit",
"statusMessage": "Refreshing Beads context",
"type": "command"
}
]
}
]
}
}

7
.gitignore vendored
View file

@ -4,3 +4,10 @@ playwright-report/
test-results/
.env
.DS_Store
.local/
# Beads / Dolt files (added by bd init)
.dolt/
*.db
.beads-credential-key
.beads/proxieddb/

View file

@ -122,3 +122,83 @@ npm run check
Après lecture du diff pré-push, proposer un exemple de message de commit en anglais, concis et représentatif des changements réellement présents.
Ne pas faire de refactor large sans bénéfice clair. Garder les corrections pré-push ciblées, vérifiables et faciles à relire.
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:970c3bf2 -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```
### Rules
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
## Agent Context Profiles
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
## Session Completion
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
```bash
# Conservative/minimal/default: report status and proposed commands; wait for approval.
git status
# Team-maintainer opt-in only, unless current instructions forbid it:
git pull --rebase
bd dolt push
git push
git status
```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->
<!-- BEGIN BEADS CODEX SETUP: generated by bd setup codex -->
## Beads Issue Tracker
Use Beads (`bd`) for durable task tracking in repositories that include it. Use the `beads` skill at `.agents/skills/beads/SKILL.md` (project install) or `~/.agents/skills/beads/SKILL.md` (global install) for Beads workflow guidance, then use the `bd` CLI for issue operations.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
bd prime # Refresh Beads context
```
### Rules
- Use `bd` for all task tracking; do not create markdown TODO lists.
- Run `bd prime` when Beads context is missing or stale. Codex 0.129.0+ can load Beads context automatically through native hooks; use `/hooks` to inspect or toggle them.
- Keep persistent project memory in Beads via `bd remember`; do not create ad hoc memory files.
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
<!-- END BEADS CODEX SETUP -->

77
CLAUDE.md Normal file
View file

@ -0,0 +1,77 @@
# Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:6cd5cc61 -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```
### Rules
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
## Agent Context Profiles
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
## Session Completion
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
```bash
# Conservative/minimal/default: report status and proposed commands; wait for approval.
git status
# Team-maintainer opt-in only, unless current instructions forbid it:
git pull --rebase
git push
git status
```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->
## Build & Test
_Add your build and test commands here_
```bash
# Example:
# npm install
# npm test
```
## Architecture Overview
_Add a brief overview of your project architecture_
## Conventions & Patterns
_Add your project-specific conventions here_

View file

@ -1,6 +1,6 @@
# Design System - Sokko G
# Nebula Design System - Sokko G
Ce document est la reference visuelle du projet Sokko G. Toute nouvelle interface doit rester coherente avec le theme **Nebula Command** actuel.
Ce document est la reference visuelle du projet Sokko G. Toute nouvelle interface doit rester coherente avec le theme **Nebula Command** actuel et les controles du **Nebula Design System**.
## 1. Vision
@ -63,7 +63,36 @@ Animations :
- `--duration-normal: 180ms`
- `--ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1)`
## 3. Fond Nebula
## 3. API Des Contrôles NDS
Le Nebula Design System expose ses contrôles publics avec une classe de famille et des attributs `data-*`.
Règles :
- la classe décrit uniquement la famille : `nds-button`, `nds-icon-button`, `nds-input`, `nds-select`, `nds-select-trigger`, `nds-time`, `nds-textarea`, `nds-inline-input`, `nds-switch`, `nds-radio-button-group`, `nds-checkbox`, `nds-quantity-stepper`, `nds-list-row`, `nds-large-icon-picker-trigger`, `nds-event-surface`, `nds-scrollbox` ;
- les variations passent par attributs : `data-size`, `data-variant`, `data-edge`, `data-border`, `data-axis` ;
- la page `/design-system` affiche aussi `data-state` comme axe de comparaison visuelle des états ;
- `data-size` accepte seulement les tailles prévues par la famille (`xs`, `sm`, `md`, `lg`, `xxs`, `micro` selon le contrôle) ; les checkboxes utilisent `md` 20 px, `sm` 18 px et `xs` 14 px ;
- `data-variant` couvre les intentions visuelles partagées : `primary`, `secondary`, `ghost`, `dashed`, `danger` et les variantes spécialisées documentées dans `/design-system`, dont `panel-toggle` pour les boutons icones accolés à un panneau et `icon-picker` pour les triggers de sélection compacts à icône ;
- `data-edge="left|middle|right|top|center|bottom|top-left|top-right|bottom-right|bottom-left"` décrit la position d'un bouton dans un groupe ou contre un bord, et ajuste uniquement les coins ;
- `data-border="joined"` masque les bordures de jonction déduites depuis `data-edge` ;
- `data-border="none"` masque toutes les bordures sans changer la taille, le fond, le hover ou l'état actif ;
- `data-axis="horizontal|vertical"` reste réservé aux composants dont l'axe est métier, comme `nds-scrollbox` ;
- les états fonctionnels restent portés par le HTML et l'application : `disabled`, `aria-pressed="true"`, `.active`, `.is-active`.
Exemples :
```html
<button class="nds-button" data-size="xs" data-variant="primary" data-edge="left" data-border="joined">Ajouter</button>
<button class="nds-icon-button" data-size="sm" data-variant="danger" aria-label="Supprimer"></button>
<button class="nds-icon-button" data-variant="panel-toggle" data-edge="right" data-border="joined" aria-pressed="true" aria-label="Pages"></button>
<select class="nds-select" data-size="sm"></select>
<button class="nds-select-trigger" data-size="sm" aria-pressed="false"></button>
```
Les anciennes classes de variation `nds-*--*` ne doivent plus etre ajoutees. Les anciennes classes globales `.button`, `.primary`, `.danger`, `.ghost` et `.import-button` ne sont plus des APIs visuelles publiques : utiliser `nds-button` / `nds-icon-button` avec `data-variant`. Dans les outils, les classes métier peuvent rester pour le layout, la grille, les largeurs, le positionnement et les états fonctionnels. Le style visuel d'un contrôle générique doit venir de `website/src/styles/design-system/_controls.scss` ou de l'API publique `website/src/styles/design-system/_attributes.scss`.
## 4. Fond Nebula
Le fond global est defini dans `website/src/styles/_nebula.scss`.
@ -76,7 +105,7 @@ Regles :
Ne pas ajouter d'orbes decoratifs supplementaires. Si une section a besoin de profondeur, utiliser les fonds semi-transparents et les bordures existantes.
## 4. Typographie
## 5. Typographie
Police principale :
@ -93,7 +122,7 @@ Regles :
- paragraphs limites visuellement, pas de longs blocs sans respiration ;
- les `<i>` doivent etre en italique, pas utilises comme gris decoratif.
## 5. Heroes
## 6. Heroes
Les heroes de la page d'accueil et des pages jeux utilisent le meme style via `.hero` et `.game-hero`.
@ -108,7 +137,7 @@ Caracteristiques :
Les pages jeux doivent heriter du hero commun. Si un jeu a une image, elle doit etre utilisee dans les cards ou les covers, pas comme remplacement complet du hero commun sauf decision explicite.
## 6. Cards
## 7. Cards
Les cards representent les elements repetes : toolboxes, jeux, categories, monstres, faune.
@ -124,7 +153,7 @@ La bordure haute/basse interne d'une cover doit rester doree quand elle separe u
Eviter les cards dans des cards. Les sections de page doivent rester des layouts ou panneaux, pas des empilements de cartes decoratives.
## 7. Modules Toolbox
## 8. Modules Toolbox
Les modules sont des panneaux de travail, pas des cards classiques.
@ -389,7 +418,7 @@ Regles :
### Selects Des Outils
Tous les `<select>` rendus dans le contenu d'un outil doivent utiliser le style factorise de `.module-content select`, defini dans `website/src/styles/toolboxes/_shared-controls.scss`.
Tous les `<select>` rendus dans le contenu d'un outil doivent utiliser le style factorise par `website/src/styles/design-system/_controls.scss`.
Regles :
@ -571,37 +600,180 @@ Notifications internes :
- duree d'affichage cible : 2 minutes, fermeture manuelle possible ;
- style plus visible que les confirmations simples : bordure conique animee, pulse leger, arrivee depuis le cote configure.
## 17. Scrollbars
## 17. Controles Interactifs
Les scrollbars reprennent l'ancien rendu :
Les champs, selects, boutons icones, switchs et choix compacts utilisent une base commune issue de `website/src/styles/design-system/_controls.scss`. Les fichiers d'outils peuvent choisir une taille ou poser une contrainte de layout, mais ne doivent pas redefinir localement les fonds, bordures, hovers ou etats `primary`/`danger`.
```css
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
}
Inputs et textareas :
::-webkit-scrollbar-track {
background-color: transparent;
box-shadow: none;
}
- utiliser `field-control` pour les champs standards ;
- la base globale cible uniquement les types texte explicites : `text`, `search`, `email`, `url`, `password` et `textarea` ;
- ne pas utiliser de selecteur global negatif comme `input:not([type="checkbox"])`, afin de ne pas capter les champs `number`, `time`, `file` ou les cellules specialisees ;
- tout nouvel `<input>` texte doit declarer son `type` explicitement ;
- utiliser `time-control` pour les champs `input[type="time"]`, avec variante `sm` ou `xs` selon le contexte ;
- taille normale : `44px` minimum ;
- taille compacte `sm` : `36px` minimum ;
- taille dense `xs` : `32px` minimum ;
- utiliser `field-textarea` pour les textareas redimensionnables avec scrollbar stable : `md` `108px`, `sm` `88px`, `xs` `72px` minimum ;
- utiliser `data-variant="ghost"` sur `nds-textarea` pour les zones d'edition intégrées à une surface métier, comme les mémos ;
- placeholders via `field-placeholder` ;
- hover via `field-hover`, focus via `field-focus` ;
- disabled via `field-disabled` ;
- les variantes complexes comme les cellules de table et le champ timer `hh:mm:ss` utilisent des mixins dedies du meme fichier.
::-webkit-scrollbar-thumb {
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.5, rgb(79, 26, 114)),
color-stop(1, rgb(44, 1, 135))
);
border-radius: 10px;
}
```
Edition inline :
Ne pas ajouter de rail noir visible ni de double bordure pres des panneaux.
- utiliser `inline-field` pour les inputs transparents au repos ;
- hover/focus via `inline-field-hover` ;
- utiliser `inline-textarea` pour les zones texte transparentes intégrées à une carte ;
- les outils peuvent changer la couleur texte selon le contexte, par exemple titre de section ou état terminé.
## 18. Images Et Assets
Selects :
- taille normale : `44px` minimum ;
- taille compacte toolbox : `36px` minimum ;
- taille dense `xs` : `32px` minimum ;
- fleche custom unique, jamais de chevrons natifs ou empiles ;
- fond sombre Nebula force via `background-color`, hover/focus violet avec chevron dore ;
- focus visible global dore conserve.
Select triggers :
- utiliser `select-trigger` pour les boutons qui ouvrent une liste, un picker ou un calendrier avec le langage visuel d'un select ;
- tailles disponibles : `md` `44px`, `sm` `36px`, `xs` `32px` ;
- le chevron est le meme asset CSS que `select-control`, pas une icone locale ;
- hover via `select-trigger-hover`, focus via `field-focus` et focus visible dore ;
- `date-picker-trigger` reste un alias de compatibilite pour les outils deja migrés.
Menus et palettes :
- utiliser `floating-control-menu` pour les menus flottants compacts ;
- utiliser `compact-menu-button` pour les options d'icones ou d'actions compactes ;
- utiliser `color-swatch`, `color-swatch-rainbow` et `color-swatch-active` pour les palettes couleur ;
- utiliser `large-icon-picker-trigger` pour les gros triggers de choix d'icone de toolbox ;
- les outils peuvent garder uniquement la position du menu, le nombre de colonnes et la variable de couleur métier.
Champs horaires :
- les champs `input[type="time"]` restent natifs ;
- utiliser `time-control` pour aligner le champ avec le theme Nebula ;
- la popup native du navigateur/OS n'est pas consideree comme stylable de facon fiable en CSS.
Boutons texte :
- utiliser `text-button` depuis `website/src/styles/design-system/_controls.scss` ;
- taille normale : hauteur minimale `42px` ;
- taille compacte `sm` : hauteur minimale `34px` ;
- taille dense `xs` : hauteur minimale `30px` ;
- le bouton standard sert de variant secondaire par defaut ;
- variants disponibles : `primary`, `secondary`, `ghost`, `dashed`, `danger` ;
- les boutons texte peuvent utiliser `data-edge` pour ajuster leurs coins et `data-border` pour gérer les bordures jointes ou masquées ;
- etat toggler via `.active` ou `.is-active`, avec le mixin `temporary-toggle-active` ;
- etat desactive via `control-disabled` ;
- les outils peuvent uniquement ajuster la largeur, l'alignement ou l'espacement environnant.
Boutons icones :
- taille normale : `40px`, icone `21px` ;
- taille compacte : `34px`, icone `19px` ;
- taille dense `xs` : `30px`, icone `17px` ;
- taille inline `xxs` : `26px`, icone `15px` ;
- tailles micro reservees aux noeuds denses : `24px`/`13px` et `22px`/`12px` ;
- fond compact sombre, rayon `--radius-md`, hover violet contenu ;
- variant `primary` dore/violet pour les actions principales ;
- variant `ghost` transparent pour les handles discrets ;
- variant `danger` rouge contenu pour les suppressions ;
- variant de forme `button-edge(left|middle|right|top|center|bottom|top-left|top-right|bottom-right|bottom-left)` pour ajuster uniquement les coins contre une bordure ou dans un groupe ;
- variant de bordure `button-joined-border(...)` pour masquer les bordures de jonction déduites depuis `data-edge` ;
- variant de bordure `button-borderless` pour masquer toutes les bordures ;
- variant visuel `panel-toggle` pour les boutons icones accolés à un panneau, comme le bouton pages ;
- etat toggler via `.active` ou `.is-active`, avec le mixin `temporary-toggle-active`.
- les barres d'outils compactes reutilisent la famille bouton icone ; `toolbar-button` reste un alias pratique pour les boutons courts du bloc-notes (`B`, `I`, `U`) et les boutons de dessin.
Boutons composes et toggles de bord :
- utiliser `button-edge` + `button-joined-border` pour les boutons groupés type ajout + chevron ;
- utiliser `button-edge` pour la forme des toggles accoles au bord d'un module, et `icon-button-panel-toggle` pour leur rendu visuel quand ils reprennent le pattern du bouton `pages` ;
- utiliser `list-row-button` pour les lignes selectionnables type pages du bloc-notes ;
- utiliser `text-button-dashed` pour les actions d'ajout contextuelles encadrees en pointilles ;
- `split-button-start`, `split-button-end`, `edge-toggle` et `dashed-action-button` restent des alias de compatibilite internes, pas des familles visuelles separees ;
- les outils peuvent garder uniquement la position absolue, la largeur contextuelle et les icones ;
- l'etat actif/hover doit rester dans le DS, sans fond/bordure locale.
Steppers de quantite :
- utiliser `quantity-stepper` depuis `website/src/styles/design-system/_controls.scss` ;
- variante normale : checklist, boutons `28px` autour de la valeur editable ;
- variante compacte : arbre d'objectifs, boutons `18px` ou `16px` selon la densite du noeud ;
- hover des boutons via `quantity-stepper-button-hover`, pas via `icon-button-hover` ;
- les outils peuvent adapter la grille environnante, mais pas redefinir les boutons `- / +`, le hover ou le champ de valeur.
Switchs binaires :
- utiliser `switch-control` depuis `website/src/styles/design-system/_controls.scss` ;
- taille `lg` : switchs de module, hauteur `40px`, rail `30 x 16px` ;
- taille normale : tool-split, hauteur `34px`, rail `34 x 18px` ;
- taille compacte `sm` : contrôles denses, hauteur `28px`, rail `24 x 14px` ;
- taille inline `xs` : timer inline, hauteur `26px`, rail `18 x 12px` ;
- les variantes natives avec `<label><input type="checkbox" />...` doivent réutiliser `native-switch-label`, `native-switch-input`, `switch-track` et `switch-thumb` ;
- les outils peuvent uniquement ajuster l'alignement ou la largeur environnante.
Radio button groups :
- la primitive commune est `radio-button-group` / `radio-button-group-option` depuis `website/src/styles/design-system/_controls.scss` ;
- tailles disponibles : `md`, `sm`, `xs` ;
- cette base couvre les tabs d'outils, switches de layout, choix d'alertes et répétitions calendrier ;
- le nombre d'options, le fait d'utiliser une icone, un texte ou les deux ne constitue pas un variant visuel ;
- les toggles compacts non natifs peuvent reutiliser ces mixins meme avec des `span`, tant que le comportement reste celui d'un choix exclusif ou d'un choix compact ;
- l'etat actif passe par `.active` ou `.is-active` avec le style toggler temporaire ;
- les outils peuvent definir la grille ou la largeur, mais pas redefinir localement les fonds, bordures, hovers ou actifs.
Controles composes :
- utiliser `compound-toggle-field` pour les cas qui combinent un choix compact et un champ, comme `Hold + number` ;
- le bouton interne utilise `compound-toggle-field-option`, base sur `radio-button-group-option(xs)` ;
- le champ interne utilise `compound-toggle-field-input`, base sur `field-control(xs)` et `number-spinner-reset` ;
- ces controles ne sont pas des variants du groupe de choix, car leur structure mélange action et saisie.
Checkboxes :
- utiliser `checkbox-control` depuis `website/src/styles/design-system/_controls.scss` ;
- taille normale globale : `20px`, format carre strict `1:1` ;
- taille compacte `xs` : `14px` pour les listes denses comme les prérequis de l'arbre d'objectifs ;
- etat coche avec gradient dore/violet et coche blanche ;
- les outils peuvent ajuster uniquement l'alignement ou l'espacement environnant.
Date picker et menus compacts :
- les tabs internes d'outils utilisent les primitives `radio-button-group-surface`, `radio-button-group-option`, `radio-button-group-option-hover` et `radio-button-group-option-active` ;
- les boutons du mini calendrier mensuel utilisent `date-picker-button` et `date-picker-button-hover` ;
- les boutons de menu/picker compacts utilisent `compact-menu-button` et `compact-menu-button-hover` ;
- les boutons transparents integres aux evenements calendrier utilisent `event-surface-button`, `event-surface-button-hover` et les contrastes `event-surface-contrast-default` / `event-surface-contrast-dark` ;
- `nds-event-surface[data-variant="default"]` cible un texte blanc/gris blanc sur fond sombre ;
- `nds-event-surface[data-variant="dark"]` cible un texte noir/gris noir sur fond clair ;
- `nds-event-surface[data-size="md"]` vise les surfaces à partir de `44px` et affiche le titre et la metadata secondaire, par exemple l'heure ;
- `nds-event-surface[data-size="sm"]` vise les surfaces autour de `22px` et masque la metadata secondaire ;
- `nds-event-surface[data-size="xs"]` vise les surfaces ultra-fines autour de `10px` et affiche uniquement le titre en taille réduite ;
- `nds-event-surface[data-color="#8b5cf6"]` expose la couleur métier de l'event ; la variable inline `--nds-event-color` applique cette couleur au fond et a la bordure ;
- le hover utilise une superposition sombre legere pour rester lisible sur une couleur dynamique inconnue du Sass ;
- les couleurs de fond des events restent métier et ne sont pas des variants NDS ;
- les fichiers d'outils peuvent conserver les dimensions de grille, le placement du menu et les états métier `disabled`.
## 18. Scrollbars
Les scrollbars utilisent `scrollbar-control` depuis `website/src/styles/design-system/_controls.scss`.
Regles :
- largeur/hauteur WebKit : `6px` ;
- `scrollbar-width: thin` pour Firefox ;
- track transparent ;
- thumb violet/indigo avec hover dore contenu ;
- coin transparent pour les zones scrollables dans les deux axes ;
- les textareas redimensionnables ajoutent `scrollbar-resizer`, sans fond noir ;
- ne pas ajouter de rail noir visible ni de double bordure pres des panneaux.
## 19. Images Et Assets
Regles :
@ -617,7 +789,7 @@ Regles :
- les categories de jeu utilisent des images representatives, entieres et bien cadrees ;
- eviter les images trop grandes dans les cards.
## 19. Accessibilite
## 20. Accessibilite
Minimum attendu :
@ -629,7 +801,7 @@ Minimum attendu :
- modales avec `role="dialog"` et `aria-modal="true"` ;
- ne pas transmettre une information uniquement par couleur.
## 20. A Eviter
## 21. A Eviter
- accents dores partout ;
- hover trop lumineux sur tous les elements ;

View file

@ -138,9 +138,9 @@ test("style entrypoint and theme layers are wired", async () => {
assert.match(styles, /@use "diablo4"/);
assert.match(styleTokens, /--gradient-brand/);
assert.match(styleBase, /:is\(/);
assert.match(styleBase, /\.card-icon-button/);
assert.match(styleBase, /\.drawer-action-button/);
assert.match(styleBase, /\.import-icon-button/);
assert.doesNotMatch(styleBase, /\.card-icon-button/);
assert.doesNotMatch(styleBase, /\.drawer-action-button/);
assert.doesNotMatch(styleBase, /\.import-icon-button/);
assert.match(styleBase, /themed-scrollbar/);
assert.match(styleBase, /body\.is-modal-open/);
assert.match(styleNebula, /@function star-field/);
@ -172,7 +172,7 @@ test("style entrypoint and theme layers are wired", async () => {
assert.match(styleToolboxCalendar, /\.calendar-event-form/);
assert.match(styleToolboxCalendar, /\.calendar-month-date-picker/);
assert.match(styleToolboxCalendar, /\.calendar-alert-mode-switch/);
assert.match(styleToolboxSharedControls, /\.notepad-toolbar-button\.danger/);
assert.match(styles, /@use "design-system\/attributes"/);
assert.match(styleToolboxLayout, /\.toolbox-icon-picker/);
assert.match(styleToolboxLayout, /\.toolbox-game-icon/);
assert.match(styleToolboxLayout, /\.toolbox-hero-link/);

View file

@ -437,7 +437,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(calendarBundle, /calendar-repeat-switch/);
assert.match(calendarBundle, /layout-switch calendar-month-repeat-switch/);
assert.match(calendarBundle, /WEEK_TIME_INPUT_STEP_MINUTES \* 60/);
assert.match(calendarBundle, /durationMinutes < 120/);
assert.match(calendarBundle, /naturalHeight/);
assert.match(calendarBundle, /data-size=\{eventSurfaceSize\}/);
assert.match(calendarBundle, /is-compact/);
assert.match(calendarController, /handleKeyDown/);
assert.match(calendarContent, /TextExchangeActions/);
@ -534,7 +535,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(checklistParts, /chevron-down/);
assert.match(checklistParts, /chevron-up/);
assert.match(checklistParts, /checklist-qty-current/);
assert.match(checklistParts, /checklist-delete-button danger/);
assert.match(checklistParts, /checklist-delete-button nds-icon-button/);
assert.match(checklistModule, /useGroupedReorder/);
assert.match(checklistBundle, /getItemProps/);
assert.match(checklistParts, /getGroupProps/);

View file

@ -89,7 +89,7 @@ export function GuidedTooltipTour({ steps, labels = {}, targetAttribute = "data-
return createPortal(
<section ref={tooltipRef} className="toolbox-tutorial-tooltip" aria-live="polite" aria-labelledby="toolbox-tutorial-title" data-placement={placement} role="dialog" style={tooltipStyle}>
<button className="toolbox-tutorial-close" type="button" onClick={onDismiss} aria-label={labels.dismiss || "Fermer le tutoriel"} title={labels.dismiss || "Fermer"}>
<button className="toolbox-tutorial-close nds-icon-button" data-size="xs" type="button" onClick={onDismiss} aria-label={labels.dismiss || "Fermer le tutoriel"} title={labels.dismiss || "Fermer"}>
<Icon name="close" />
</button>
<span className="toolbox-tutorial-count">{stepIndex + 1}/{steps.length}</span>
@ -99,7 +99,7 @@ export function GuidedTooltipTour({ steps, labels = {}, targetAttribute = "data-
</div>
<p>{currentStep.text}</p>
<footer>
<button className="primary" type="button" onClick={isLastStep ? onDismiss : () => setStepIndex((index) => Math.min(index + 1, steps.length - 1))}>
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={isLastStep ? onDismiss : () => setStepIndex((index) => Math.min(index + 1, steps.length - 1))}>
{isLastStep ? labels.done || "Terminer" : labels.next || "Suivant"}
</button>
</footer>

View file

@ -142,9 +142,9 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
{image.label && <strong className="image-viewer-title">{image.label}</strong>}
<div className="image-viewer-actions">
{!canAnnotate && !hasMarkers && (
<button className="image-viewer-button" onClick={() => openImageInNewTab(image.dataUrl)} aria-label="Ouvrir l'image en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
<button className="image-viewer-button nds-icon-button" data-size="sm" onClick={() => openImageInNewTab(image.dataUrl)} aria-label="Ouvrir l'image en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
)}
<button className="image-viewer-button" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
<button className="image-viewer-button nds-icon-button" data-size="sm" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
</div>
</header>
<div className={`image-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
@ -211,12 +211,15 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
<li key={marker.id}>
<span>{index + 1}</span>
<input
className="nds-input"
data-size="sm"
type="text"
value={marker.label}
placeholder={image.markerPlaceholder || "Libellé du marqueur"}
onChange={(event) => updateViewerMarker(marker.id, (item) => ({ ...item, label: event.target.value }))}
aria-label={`Libellé du marqueur ${index + 1}`}
/>
<button className="tool-split-action-button danger" type="button" onClick={() => removeViewerMarker(marker.id)} aria-label={`${image.deleteMarkerTitle || "Supprimer"} ${marker.label || `${image.markerPrefix || "Marqueur"} ${index + 1}`}`} title={image.deleteMarkerTitle || "Supprimer"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => removeViewerMarker(marker.id)} aria-label={`${image.deleteMarkerTitle || "Supprimer"} ${marker.label || `${image.markerPrefix || "Marqueur"} ${index + 1}`}`} title={image.deleteMarkerTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</li>

View file

@ -1,9 +1,9 @@
// Rôle : encapsule l'input fichier JSON derrière un bouton d'import réutilisable.
import { Icon } from "./Icon.jsx";
export function ImportButton({ className, label, title, ariaLabel, icon, onFile }) {
export function ImportButton({ className, label, title, ariaLabel, icon, onFile, ...props }) {
return (
<label className={className} aria-label={ariaLabel} title={title}>
<label className={className} aria-label={ariaLabel} title={title} {...props}>
{icon && <Icon name={icon} />}
{label}
<input type="file" accept="application/json" hidden onChange={async (event) => {

View file

@ -6,7 +6,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
const gameId = route.split("/")[1] === "games" ? route.split("/")[2] || "" : "";
const game = games.find((item) => item.id === gameId);
const linkedToolbox = game ? toolboxes.find((item) => item.id === links[game.id]) : null;
const topbarLabel = route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
const topbarLabel = route === "/design-system" && import.meta.env.DEV ? "Design system" : route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
const toolboxLabel = linkedToolbox ? `Ouvrir la toolbox ${linkedToolbox.name}` : "Associer une toolbox";
return (
@ -27,13 +27,16 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<div className="sidebar-secondary-links">
<a className={`sidebar-about-link ${route === "/about" ? "active" : ""}`} href="#/about"><span className="sidebar-about-icon" aria-hidden="true">?</span><strong>{content.navigation.about}</strong></a>
<a className={`sidebar-about-link ${route === "/library" ? "active" : ""}`} href="#/library"><span className="sidebar-about-icon" aria-hidden="true"><Icon name="book" /></span><strong>{content.library.eyebrow}</strong></a>
{import.meta.env.DEV && (
<a className={`sidebar-about-link ${route === "/design-system" ? "active" : ""}`} href="#/design-system"><span className="sidebar-about-icon" aria-hidden="true"><Icon name="settings" /></span><strong>Design system</strong></a>
)}
</div>
<div className="sidebar-note">
<span className="badge">{content.sidebar.badge}</span>
<p>{content.sidebar.note}</p>
<div className="sidebar-storage-actions" aria-label="Actions globales de stockage">
<button className="sidebar-action-button primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" data-toolbox-tutorial-target="exportAll" title="Exporter tout"><Icon name="export" /></button>
<ImportButton className="sidebar-action-button import-icon-button" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
<button className="sidebar-action-button nds-icon-button" data-size="sm" data-variant="primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" data-toolbox-tutorial-target="exportAll" title="Exporter tout"><Icon name="export" /></button>
<ImportButton className="sidebar-action-button nds-icon-button import-icon-button" data-size="sm" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
</div>
</div>
</aside>
@ -42,7 +45,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<div><strong>{topbarLabel}</strong></div>
{game && (
<div className="topbar-actions">
<button className="drawer-button toolbox-icon-button primary" onClick={() => actions.setDrawerGameId(game.id)} aria-label={toolboxLabel} title={toolboxLabel}>
<button className="drawer-button toolbox-icon-button nds-icon-button" data-size="sm" data-variant="primary" onClick={() => actions.setDrawerGameId(game.id)} aria-label={toolboxLabel} title={toolboxLabel}>
<img src="/static/icons/toolbox.svg" alt="" aria-hidden="true" />
</button>
</div>
@ -53,7 +56,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<nav className="mobile-nav" aria-label="Navigation mobile">
<a className={route === "/" ? "active" : ""} href="#/">{content.navigation.home}</a>
<a className={route.startsWith("/toolboxes") || route.startsWith("/toolbox") ? "active" : ""} href="#/toolboxes">{content.navigation.toolboxes}</a>
<button className="primary" onClick={() => actions.setCreateModal({})}>+</button>
<button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({})}>+</button>
<a className={route === "/library" ? "active" : ""} href="#/library">{content.navigation.mobileLibrary}</a>
<a className={route.startsWith("/games") ? "active" : ""} href="#/games">{content.navigation.mobileGames}</a>
</nav>

View file

@ -1,7 +1,7 @@
// Rôle : fournit un contrôle d'onglets réutilisable avec rôles ARIA.
export function Tabs({ tabs, activeTab, onChange, className = "", iconOnly = false }) {
export function Tabs({ tabs, activeTab, onChange, className = "", iconOnly = false, size = "sm" }) {
return (
<div className={`tabs ${className}`.trim()} role="tablist">
<div className={`tabs nds-radio-button-group ${className}`.trim()} data-size={size} role="tablist">
{tabs.map((tab) => {
const active = tab.id === activeTab;
return (

View file

@ -4,16 +4,34 @@ import { Icon } from "./Icon.jsx";
export function ToastPositionSwitch({ position, labels, onChange }) {
const normalizedPosition = position === "left" ? "left" : "right";
return (
<button
className="toast-position-switch"
type="button"
<div
className="toast-position-switch nds-radio-button-group"
data-size="xs"
role="radiogroup"
data-position={normalizedPosition}
onClick={() => onChange(normalizedPosition === "left" ? "right" : "left")}
aria-label={normalizedPosition === "left" ? labels.left : labels.right}
title={labels.title}
>
<span><Icon name="toast-left" /></span>
<span><Icon name="toast-right" /></span>
</button>
<button
className={normalizedPosition === "left" ? "active" : ""}
type="button"
role="radio"
aria-checked={normalizedPosition === "left"}
aria-label={labels.left}
onClick={() => onChange("left")}
>
<Icon name="toast-left" />
</button>
<button
className={normalizedPosition === "right" ? "active" : ""}
type="button"
role="radio"
aria-checked={normalizedPosition === "right"}
aria-label={labels.right}
onClick={() => onChange("right")}
>
<Icon name="toast-right" />
</button>
</div>
);
}

View file

@ -16,8 +16,8 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance
<header><h2 id="confirm-title">{title}</h2></header>
<p id="confirm-message">{message}</p>
<footer>
<button onClick={() => onClose(false)}>{cancelLabel}</button>
<button className={danger ? "danger confirm-danger" : "primary"} onClick={() => onClose(true)}>{confirmLabel}</button>
<button className="nds-button" data-size="sm" onClick={() => onClose(false)}>{cancelLabel}</button>
<button className="confirm-danger nds-button" data-size="sm" data-variant={danger ? "danger" : "primary"} onClick={() => onClose(true)}>{confirmLabel}</button>
</footer>
</section>
</div>
@ -33,7 +33,7 @@ export function NotificationToast({ id, message, durationMs = 120000, onClose })
return (
<div className="notification-toast" role="status">
<span>{message}</span>
<button type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer">
<button className="nds-icon-button" data-size="xs" type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer">
<Icon name="close" />
</button>
</div>
@ -52,8 +52,8 @@ export function CreateToolboxModal({ gameId, initialName = "", onClose }) {
event.preventDefault();
if (name.trim()) onClose(name.trim());
}}>
<label className="field"><span>Nom</span><input name="name" autoComplete="off" placeholder="Nom de la toolbox" required value={name} onChange={(event) => setName(event.target.value)} autoFocus /></label>
<footer><button type="button" onClick={() => onClose("")}>Annuler</button><button className="primary" type="submit">Créer</button></footer>
<label className="field"><span>Nom</span><input className="nds-input" data-size="sm" name="name" type="text" autoComplete="off" placeholder="Nom de la toolbox" required value={name} onChange={(event) => setName(event.target.value)} autoFocus /></label>
<footer><button className="nds-button" data-size="sm" type="button" onClick={() => onClose("")}>Annuler</button><button className="nds-button" data-size="sm" data-variant="primary" type="submit">Créer</button></footer>
</form>
</section>
</div>
@ -72,8 +72,8 @@ export function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
event.preventDefault();
onClose(toolboxId);
}}>
<label className="field"><span>Toolbox associée</span><select value={toolboxId} onChange={(event) => setToolboxId(event.target.value)} autoFocus><option value="">Aucune</option>{toolboxes.map((toolbox) => <option key={toolbox.id} value={toolbox.id}>{toolbox.name}</option>)}</select></label>
<footer><button type="button" onClick={() => onClose(null)}>Annuler</button><button className="primary" type="submit">Valider</button></footer>
<label className="field"><span>Toolbox associée</span><select className="nds-select" data-size="sm" value={toolboxId} onChange={(event) => setToolboxId(event.target.value)} autoFocus><option value="">Aucune</option>{toolboxes.map((toolbox) => <option key={toolbox.id} value={toolbox.id}>{toolbox.name}</option>)}</select></label>
<footer><button className="nds-button" data-size="sm" type="button" onClick={() => onClose(null)}>Annuler</button><button className="nds-button" data-size="sm" data-variant="primary" type="submit">Valider</button></footer>
</form>
</section>
</div>

View file

@ -57,7 +57,8 @@ export function CopyChecklistItemsButton({ labels, sections, title = "Copier pou
return (
<button
className="filter-reset-button results-copy-button"
className="filter-reset-button results-copy-button nds-icon-button"
data-size="xs"
type="button"
onClick={copyItems}
disabled={!text}

View file

@ -17,8 +17,6 @@ export function GameFiltersPanel({
onChange
}) {
const selected = new Set(filters[selectedKey] || []);
const logicLabel = filters.logic === "or" ? logicLabels.or : logicLabels.and;
function update(updater) {
onChange(updater(filters));
}
@ -31,7 +29,8 @@ export function GameFiltersPanel({
<h2>{title}</h2>
</div>
<button
className="filter-reset-button"
className="filter-reset-button nds-icon-button"
data-size="xs"
onClick={() => onChange({ name: "", [selectedKey]: [], logic: "and" })}
aria-label={resetLabel}
title={resetLabel}
@ -41,11 +40,24 @@ export function GameFiltersPanel({
</div>
<label className="field compact">
<span>{nameLabel}</span>
<input value={filters.name} placeholder="Rechercher..." onChange={(event) => update((state) => ({ ...state, name: event.target.value }))} />
<input className="nds-input" data-size="sm" type="search" value={filters.name} placeholder="Rechercher..." onChange={(event) => update((state) => ({ ...state, name: event.target.value }))} />
</label>
<div className="filter-logic">
<span>{matchLabel}</span>
<button onClick={() => update((state) => ({ ...state, logic: state.logic === "and" ? "or" : "and" }))}>{logicLabel}</button>
<div className="filter-logic-group nds-radio-button-group" data-size="xs" role="radiogroup" aria-label={matchLabel}>
{["and", "or"].map((logic) => (
<button
key={logic}
className={filters.logic === logic ? "active" : ""}
type="button"
role="radio"
aria-checked={filters.logic === logic}
onClick={() => update((state) => ({ ...state, logic }))}
>
{logicLabels[logic]}
</button>
))}
</div>
</div>
<div className="filter-options legacy-scrollbar">
{options.map((option) => {
@ -53,6 +65,8 @@ export function GameFiltersPanel({
return (
<label className={`filter-chip ${getOptionClassName(option)} ${selected.has(optionId) ? "active" : ""}`} key={optionId}>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={selected.has(optionId)}
onChange={(event) => update((state) => {

View file

@ -30,7 +30,7 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
<h1>Listes</h1>
</div>
<p>Sélectionnez une liste, puis copiez-la ou créez une checklist dans la toolbox associée.</p>
<a className="game-list-help-link button" href="#/about#contribuer">Proposer une nouvelle liste</a>
<a className="game-list-help-link nds-button" data-size="sm" href="#/about#contribuer">Proposer une nouvelle liste</a>
</div>
</section>
{lists.length ? (
@ -43,7 +43,9 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
<small>{list.itemCount} éléments</small>
</button>
<button
className="game-list-create-button primary"
className="game-list-create-button nds-icon-button"
data-size="sm"
data-variant="primary"
type="button"
disabled={!linkedToolboxId}
onClick={() => onCreateChecklist?.(list)}

View file

@ -32,7 +32,7 @@ export function GamesPage({ siteContent, games, gamesError }) {
<h2>{game.title}</h2>
<p>{game.summary}</p>
<div className="card-actions">
<a className="button primary" href={`#/games/${game.id}`}>Ouvrir</a>
<a className="nds-button" data-size="sm" data-variant="primary" href={`#/games/${game.id}`}>Ouvrir</a>
</div>
</div>
</article>

View file

@ -30,8 +30,8 @@ export function MhwildsListing({ category, game, mhwilds, filters, setFilters, t
<p>{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}</p>
</div>
<div className="actions">
<a className={`button ${isMonsters ? "primary" : ""}`} href="#/games/mhwilds/monsters">Monstres</a>
<a className={`button ${!isMonsters ? "primary" : ""}`} href="#/games/mhwilds/endemic">Faune</a>
<a className="nds-button" data-size="sm" data-variant={isMonsters ? "primary" : "secondary"} href="#/games/mhwilds/monsters">Monstres</a>
<a className="nds-button" data-size="sm" data-variant={!isMonsters ? "primary" : "secondary"} href="#/games/mhwilds/endemic">Faune</a>
</div>
</section>
<section className="game-layout" data-mhwilds-category={category}>

View file

@ -34,7 +34,9 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
return (
<article className={className} {...reorder.getItemProps({ itemId: toolbox.id })}>
<button
className="toolbox-card-drag-handle"
className="toolbox-card-drag-handle nds-icon-button"
data-size="md"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, toolbox.id)}
aria-label={`Déplacer ${toolbox.name}`}
@ -56,9 +58,9 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
<h2>{toolbox.name}</h2>
<small>Modifiée le {formatDate(toolbox.updatedAt)}</small>
<div className="card-actions">
<a className="button card-icon-button" href={`#/toolbox/${toolbox.id}`} aria-label={`Ouvrir ${toolbox.name}`} title="Ouvrir"><Icon name="open" /></a>
<button className="card-icon-button" onClick={() => actions.exportToolbox(toolbox.id)} aria-label={`Exporter ${toolbox.name}`} title="Exporter"><Icon name="export" /></button>
<button className="card-icon-button danger" onClick={() => actions.setConfirmModal({
<a className="card-icon-button nds-icon-button" data-size="md" href={`#/toolbox/${toolbox.id}`} aria-label={`Ouvrir ${toolbox.name}`} title="Ouvrir"><Icon name="open" /></a>
<button className="card-icon-button nds-icon-button" data-size="md" onClick={() => actions.exportToolbox(toolbox.id)} aria-label={`Exporter ${toolbox.name}`} title="Exporter"><Icon name="export" /></button>
<button className="card-icon-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => actions.setConfirmModal({
title: "Supprimer la toolbox",
message: `Supprimer "${toolbox.name}" et ses données locales ?`,
confirmLabel: "Supprimer",
@ -81,11 +83,12 @@ export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
label="Icônes de toolbox"
renderTrigger={({ open, toggle }) => (
<button
className="toolbox-icon-picker-button"
className={`toolbox-icon-picker-button nds-large-icon-picker-trigger ${open ? "active" : ""}`}
type="button"
onClick={toggle}
aria-label="Changer l'icône de la toolbox"
aria-expanded={open}
aria-pressed={open}
data-toolbox-tutorial-target={tutorialTarget || undefined}
title="Changer l'icône"
>
@ -98,7 +101,8 @@ export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
{TOOLBOX_ICONS.map((path) => (
<button
key={path}
className={path === icon ? "active" : ""}
className={`nds-icon-button ${path === icon ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => {
onChange(path);

View file

@ -102,17 +102,17 @@ export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions,
<p className="eyebrow">{content.eyebrow}</p>
<h1>{content.title}</h1>
<p>{content.summary}</p>
<a className="button hero-library-button library-link-button" href="#/library"><Icon name="book" />{content.libraryLink}</a>
<a className="hero-library-button library-link-button nds-button" data-size="sm" href="#/library"><Icon name="book" />{content.libraryLink}</a>
</div>
</section>
<section className="toolbar toolbox-actions-row">
<div>
<button className="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
<ImportButton className="import-button" label={content.importOne} onFile={actions.importToolbox} />
<button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
<ImportButton className="nds-button" data-size="sm" label={content.importOne} onFile={actions.importToolbox} />
</div>
<div>
<button className="primary" onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
<ImportButton className="import-button" label={content.importAll} onFile={actions.importAllToolboxes} />
<button className="nds-button" data-size="sm" data-variant="primary" onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
<ImportButton className="nds-button" data-size="sm" label={content.importAll} onFile={actions.importAllToolboxes} />
</div>
</section>
<section className="toolbox-preferences-row" aria-label={content.preferencesLabel}>
@ -169,7 +169,7 @@ function StorageHelpCard({ help }) {
export function ToolboxPage(props) {
const { toolboxId, toolboxes, getToolboxGame } = props;
const toolbox = toolboxes.find((item) => item.id === toolboxId);
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="button" href="#/toolboxes">Retour</a></div>;
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="nds-button" data-size="sm" href="#/toolboxes">Retour</a></div>;
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
}
@ -265,7 +265,8 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
</div>
{toolboxGame && (
<a
className="button toolbox-hero-link"
className="toolbox-hero-link nds-button"
data-size="sm"
href={embedded ? `#/toolbox/${toolbox.id}` : `#/games/${toolboxGame.id}`}
onClick={embedded ? () => actions.setDrawerGameId("") : undefined}
>
@ -299,7 +300,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
)}
{!embedded && (
<section className="modules-toolbar" aria-label="Options d'affichage des outils">
<div className="layout-switch" role="group" aria-label="Mode d'affichage des outils">
<div className="layout-switch nds-radio-button-group" data-size="sm" role="group" aria-label="Mode d'affichage des outils">
<button className={moduleColumns === 1 ? "active" : ""} onClick={() => updateToolbox({ ...toolbox, moduleColumns: 1 })} aria-pressed={moduleColumns === 1} aria-label="Afficher en lignes" title="Afficher en lignes"><Icon name="rows" /></button>
<button className={moduleColumns === 2 ? "active" : ""} onClick={() => updateToolbox({ ...toolbox, moduleColumns: 2 })} aria-pressed={moduleColumns === 2} aria-label="Afficher en colonnes" title="Afficher en colonnes"><Icon name="columns" /></button>
</div>
@ -387,15 +388,15 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
<div>
<p className="eyebrow">Toolbox liée</p>
<div className="drawer-toolbox-actions" aria-label="Actions toolbox liée">
<button className="drawer-action-button" onClick={() => actions.setLinkModalGameId(gameId)} aria-label="Lier une toolbox" title="Lier"><Icon name="link" /></button>
<ImportButton className="drawer-action-button import-icon-button" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
<button className="drawer-action-button" onClick={() => actions.exportToolbox(selectedId)} disabled={!selectedId} aria-label="Exporter la toolbox liée" title="Exporter"><Icon name="export" /></button>
<button className="drawer-action-button nds-icon-button" data-size="sm" onClick={() => actions.setLinkModalGameId(gameId)} aria-label="Lier une toolbox" title="Lier"><Icon name="link" /></button>
<ImportButton className="drawer-action-button nds-icon-button import-icon-button" data-size="sm" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
<button className="drawer-action-button nds-icon-button" data-size="sm" onClick={() => actions.exportToolbox(selectedId)} disabled={!selectedId} aria-label="Exporter la toolbox liée" title="Exporter"><Icon name="export" /></button>
</div>
</div>
<button className="drawer-close-button" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
</header>
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} /> : (
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
)}
<StorageQuota usage={storageUsage} />
</div>

View file

@ -27,7 +27,11 @@ export function AddToolControls({ onAdd }) {
aria-label="Ajouter un outil"
>
<button
className="primary tool-add-modal-button"
className="tool-add-modal-button nds-button"
data-size="sm"
data-variant="primary"
data-edge="left"
data-border="joined"
type="button"
onClick={() => setOpen(true)}
data-toolbox-tutorial-target="addTool"
@ -38,11 +42,13 @@ export function AddToolControls({ onAdd }) {
className="tool-add-quick-dropdown"
menuClassName="tool-quick-add"
label="Ajout rapide"
openOnHover
closeOnMouseLeave
renderTrigger={({ open: quickOpen, toggle }) => (
<button
className={`primary tool-add-quick-toggle ${quickOpen ? "active" : ""}`}
className={`tool-add-quick-toggle nds-icon-button ${quickOpen ? "active" : ""}`}
data-size="sm"
data-variant="primary"
data-edge="right"
data-border="joined"
type="button"
onClick={toggle}
aria-label="Afficher l'ajout rapide"
@ -58,7 +64,8 @@ export function AddToolControls({ onAdd }) {
{modules.map(([type, module]) => (
<button
key={type}
className="tool-quick-add-button"
className="tool-quick-add-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => {
onAdd(type);
@ -82,7 +89,7 @@ export function AddToolControls({ onAdd }) {
<section className="confirm-modal tool-add-modal">
<header>
<h2 id="tool-add-modal-title">Ajouter un outil</h2>
<button className="drawer-close-button" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
<button className="drawer-close-button nds-icon-button" data-size="sm" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
<Icon name="close" />
</button>
</header>

View file

@ -41,7 +41,9 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
<li className={className} key={entry.id} {...reorder.getItemProps({ itemId: entry.id, parentId: getScopedParentId(parentId) })}>
<div className="tool-split-entry calculator-entry has-drag-handle">
<button
className="calculator-drag-handle"
className="calculator-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, entry.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${entry.label || formatResult(entry.value)}`}
@ -58,6 +60,8 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
</span>
</button>
<button
className="nds-icon-button"
data-size="xs"
type="button"
onClick={() => onAddChild(entry)}
aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
@ -65,7 +69,7 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
>
<Icon name="add" />
</button>
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -228,8 +228,10 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
/>
) : (
<input
type="text"
aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input"
className="calculator-expression-input nds-input"
data-size="sm"
ref={expressionInputRef}
onChange={(event) => setExpression(event.target.value)}
onKeyDown={(event) => {
@ -251,9 +253,9 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
</div>
<label>
<span>{textContent.labelLabel || "Libellé"}</span>
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
<input className="nds-input" data-size="sm" type="text" value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
</label>
<button className="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
</div>
</form>
<div
@ -262,18 +264,19 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
>
<div className="tool-split-actions">
{(formulaParent || editingEntry) && (
<button className="tool-split-root-button" type="button" onClick={returnToRoot}>
<button className="tool-split-root-button nds-button" data-size="xs" type="button" onClick={returnToRoot}>
{editingEntry ? textContent.newButton || "Nouveau calcul" : textContent.rootButton || "Revenir à la racine"}
</button>
)}
<button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
<button className="tool-split-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label={textContent.copyTitle || "Copier pour checklist"} title={copied ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier pour checklist"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label={textContent.copyTitle || "Copier pour checklist"} title={copied ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier pour checklist"}>
<Icon name="copy" />
</button>
<button
className={`tool-split-scroll-toggle ${data.scrollResults ? "active" : ""}`}
className={`tool-split-scroll-toggle nds-switch ${data.scrollResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollResults(!data.scrollResults)}
aria-pressed={data.scrollResults}

View file

@ -130,7 +130,8 @@ function CalendarWeekOptions({ textContent, hideEmptyHours, onToggle }) {
<div className="calendar-week-options">
<span>{textContent.hideEmptyHoursLabel || "Masquer les heures vides"}</span>
<button
className={`tool-split-scroll-toggle calendar-hide-hours-switch ${hideEmptyHours ? "active" : ""}`}
className={`tool-split-scroll-toggle calendar-hide-hours-switch nds-switch ${hideEmptyHours ? "active" : ""}`}
data-size="md"
type="button"
onClick={onToggle}
aria-pressed={hideEmptyHours}

View file

@ -2,19 +2,11 @@
import { Icon } from "../../../components/Icon.jsx";
import {
CALENDAR_ALERT_OFFSETS,
CALENDAR_COLORS
CALENDAR_COLORS,
CALENDAR_COLOR_VALUES
} from "./calendarUtils.js";
import { ALERT_MODES } from "./timerOptions.js";
const CALENDAR_COLOR_VALUES = {
violet: "#8b5cf6",
gold: "#f6c453",
cyan: "#22d3ee",
pink: "#d946ef",
green: "#22c55e",
gray: "#7d879f"
};
export function MonthlyRepeatSwitch({ value, textContent, onChange }) {
const options = [
{ value: "monthDay", icon: "day-number", label: textContent.monthlyRepeatMonthDay || "Numéro du jour" },
@ -22,7 +14,7 @@ export function MonthlyRepeatSwitch({ value, textContent, onChange }) {
];
return (
<div className="layout-switch calendar-month-repeat-switch" role="group" aria-label={textContent.monthlyRepeatLabel || "Règle mensuelle"}>
<div className="layout-switch calendar-month-repeat-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.monthlyRepeatLabel || "Règle mensuelle"}>
{options.map((option) => (
<button
key={option.value}
@ -49,7 +41,7 @@ export function MonthEventRepeatSwitch({ value, monthlyRepeatBy, textContent, on
const safeValue = monthlyRepeatBy === "monthDay" && value === "weekly" ? "none" : value;
return (
<div className="calendar-month-event-repeat-switch" role="group" aria-label={textContent.repeatLabel || "Répétition"}>
<div className="calendar-month-event-repeat-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.repeatLabel || "Répétition"}>
{options.map((option) => (
<button
key={option.value}
@ -80,7 +72,7 @@ export function CalendarAlertOffsets({ selectedOffsets, textContent, onToggle })
<legend>{textContent.alertOffsetLabel || "Alerte"}</legend>
{CALENDAR_ALERT_OFFSETS.map((offset) => (
<label key={offset}>
<input type="checkbox" checked={selectedOffsets.includes(offset)} onChange={() => onToggle(offset)} />
<input className="nds-checkbox" data-size="sm" type="checkbox" checked={selectedOffsets.includes(offset)} onChange={() => onToggle(offset)} />
<span>{labels[offset]}</span>
</label>
))}
@ -94,7 +86,7 @@ export function CalendarColorPicker({ value, textContent, onChange }) {
<div className="calendar-color-picker notepad-color-dropdown">
<span>{label}</span>
<button
className="calendar-color-toggle notepad-color-toggle"
className="calendar-color-toggle notepad-color-toggle nds-swatch"
type="button"
title={label}
aria-label={label}
@ -104,7 +96,7 @@ export function CalendarColorPicker({ value, textContent, onChange }) {
{CALENDAR_COLORS.map((color) => (
<button
key={color}
className={`notepad-swatch ${value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${value === color ? "active" : ""}`}
type="button"
title={textContent[`color${color.charAt(0).toUpperCase()}${color.slice(1)}`] || color}
aria-label={textContent[`color${color.charAt(0).toUpperCase()}${color.slice(1)}`] || color}
@ -126,7 +118,7 @@ export function CalendarAlertModeSwitch({ mode, textContent, onChange }) {
};
return (
<div className="timer-alert-mode-switch calendar-alert-mode-switch" role="group" aria-label={textContent.alertModeTitle || "Mode d'alerte"}>
<div className="timer-alert-mode-switch calendar-alert-mode-switch nds-radio-button-group" data-size="xs" role="group" aria-label={textContent.alertModeTitle || "Mode d'alerte"}>
{ALERT_MODES.map((option) => (
<button
key={option.mode}

View file

@ -29,14 +29,14 @@ export function CalendarEventForm({
<div className="calendar-form-row calendar-form-main-row">
<label className="calendar-title-field">
<span>{textContent.titleLabel || "Titre"}</span>
<input value={draft.title} onChange={(event) => onDraftChange({ title: event.target.value })} placeholder={textContent.titlePlaceholder || "Raid, reset, event..."} />
<input className="nds-input" data-size="sm" type="text" value={draft.title} onChange={(event) => onDraftChange({ title: event.target.value })} placeholder={textContent.titlePlaceholder || "Raid, reset, event..."} />
</label>
<CalendarColorPicker value={draft.color} textContent={textContent} onChange={(color) => onDraftChange({ color })} />
{draft.id && <div className="calendar-delete-actions">
{data.viewMode === "month" && draft.repeat !== "none" && activeOccurrenceDate && (
<button className="danger calendar-form-delete-button" type="button" onClick={() => onDeleteOccurrence(draft.id)} aria-label={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"} title={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"}><Icon name="trashcan-one" /></button>
<button className="calendar-form-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteOccurrence(draft.id)} aria-label={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"} title={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"}><Icon name="trashcan-one" /></button>
)}
<button className="danger calendar-form-delete-button" type="button" onClick={() => onDeleteEvent(draft.id)} aria-label={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"} title={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"}><Icon name="trash" /></button>
<button className="calendar-form-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteEvent(draft.id)} aria-label={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"} title={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"}><Icon name="trash" /></button>
</div>}
</div>
<div className={`calendar-form-row ${data.viewMode === "month" ? "calendar-form-month-row" : "calendar-form-time-row"}`}>
@ -67,6 +67,8 @@ export function CalendarEventForm({
/>
) : (
<select
className="nds-select"
data-size="sm"
value={draft.dayIndex}
onChange={(event) => {
const value = Number(event.target.value);
@ -97,11 +99,11 @@ export function CalendarEventForm({
<>
<label className="calendar-time-field">
<span>{textContent.startLabel || "Début"}</span>
<input type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.startTime} onChange={(event) => onDraftChange({ startTime: event.target.value })} />
<input className="nds-time" data-size="xs" type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.startTime} onChange={(event) => onDraftChange({ startTime: event.target.value })} />
</label>
<label className="calendar-time-field">
<span>{textContent.endLabel || "Fin"}</span>
<input type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.endTime} onChange={(event) => onDraftChange({ endTime: event.target.value })} />
<input className="nds-time" data-size="xs" type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.endTime} onChange={(event) => onDraftChange({ endTime: event.target.value })} />
</label>
</>
)}
@ -113,7 +115,8 @@ export function CalendarEventForm({
) : <div className="calendar-repeat-field">
<span>{textContent.repeatableTitle || "Répétable"}</span>
<button
className={`tool-split-scroll-toggle calendar-repeat-switch ${draft.repeatable !== false ? "active" : ""}`}
className={`tool-split-scroll-toggle calendar-repeat-switch nds-switch ${draft.repeatable !== false ? "active" : ""}`}
data-size="lg"
type="button"
onClick={() => onDraftChange({ repeatable: draft.repeatable === false })}
aria-pressed={draft.repeatable !== false}
@ -131,8 +134,8 @@ export function CalendarEventForm({
<CalendarAlertModeSwitch mode={draft.alertMode} textContent={textContent} onChange={onAlertModeChange} />
</div>
<div className="calendar-form-actions">
<button className="primary" type="submit">{draft.id ? textContent.saveButton || "Enregistrer" : textContent.addButton || "Ajouter"}</button>
<button type="button" onClick={onClose}>{textContent.cancelButton || "Annuler"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{draft.id ? textContent.saveButton || "Enregistrer" : textContent.addButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>{textContent.cancelButton || "Annuler"}</button>
</div>
</div>
{error && <p className="timer-form-error calendar-form-error" role="alert"><Icon name="warning" /><span>{error}</span></p>}

View file

@ -48,7 +48,7 @@ export function MonthDateDropdown({ value, displayedMonth, minDate = "", mode, t
const safeValue = normalizeDraftDateKey(value) || formatLocalDateKey(displayedMonth);
return (
<div className={`calendar-month-date-dropdown ${align === "end" ? "is-aligned-end" : ""}`}>
<button className="calendar-month-date-trigger" type="button" aria-label={label}>
<button className="calendar-month-date-trigger nds-date-trigger" data-size="sm" type="button" aria-label={label}>
<Icon name={mode === "weekday" ? "day-name" : "day-number"} />
<span>{formatMonthDateLabel(safeValue, mode)}</span>
<Icon name="chevron-down" />

View file

@ -4,6 +4,8 @@ import {
WEEK_SLOT_MINUTES,
addDays,
formatLocalDateKey,
getCalendarColorValue,
getCalendarEventSurfaceVariant,
minutesToTimeValue,
pad2
} from "./calendarUtils.js";
@ -137,19 +139,26 @@ function CalendarOccurrence({ occurrence, layout, active, onEdit }) {
const durationMinutes = Math.max(WEEK_SLOT_MINUTES, (occurrence.endMs - occurrence.startMs) / 60000 || WEEK_SLOT_MINUTES);
const startMinutes = occurrence.event.startMinutes;
const timeLabel = `${minutesToTimeValue(occurrence.event.startMinutes)}${occurrence.event.endMinutes ? ` - ${minutesToTimeValue(occurrence.event.endMinutes)}` : ""}`;
const compact = durationMinutes < 120;
const top = getWeekMinutePosition(layout, startMinutes);
const bottom = getWeekMinutePosition(layout, Math.min(24 * 60, startMinutes + durationMinutes));
const height = compact ? WEEK_COMPACT_EVENT_HEIGHT : Math.max(WEEK_COMPACT_EVENT_HEIGHT, bottom - top);
const naturalHeight = bottom - top;
const eventSurfaceSize = naturalHeight < WEEK_COMPACT_EVENT_HEIGHT ? "xs" : naturalHeight < 44 ? "sm" : "md";
const height = eventSurfaceSize === "xs" ? 10 : eventSurfaceSize === "sm" ? WEEK_COMPACT_EVENT_HEIGHT : Math.max(44, naturalHeight);
const color = occurrence.event.color || "violet";
return (
<article
className={`calendar-event calendar-event-${occurrence.event.color || "violet"} ${active ? "is-active" : ""} ${compact ? "is-compact" : ""}`}
className={`calendar-event nds-event-surface ${active ? "is-active" : ""} ${eventSurfaceSize !== "md" ? "is-compact" : ""}`}
data-color={getCalendarColorValue(color)}
data-size={eventSurfaceSize}
data-variant={getCalendarEventSurfaceVariant(color)}
style={{
top: `${top}px`,
width: "calc((100% / var(--calendar-event-lane-count, 1)) - 2px)",
height: `${height}px`,
"--calendar-event-lane": occurrence.lane || 0,
"--calendar-event-lane-count": occurrence.laneCount || 1
"--calendar-event-lane-count": occurrence.laneCount || 1,
"--nds-event-color": getCalendarColorValue(color)
}}
>
<Tooltip
@ -161,7 +170,7 @@ function CalendarOccurrence({ occurrence, layout, active, onEdit }) {
onClick={onEdit}
>
<strong>{occurrence.event.title}</strong>
{!compact && <span>{timeLabel}</span>}
<span>{timeLabel}</span>
</Tooltip>
</article>
);
@ -188,10 +197,10 @@ export function CalendarMonthView({
return (
<>
<div className="calendar-month-toolbar">
<button type="button" onClick={onPreviousMonth} title={textContent.previousMonthTitle || "Mois précédent"} aria-label={textContent.previousMonthTitle || "Mois précédent"}>{textContent.previousShortLabel || "Prec."}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onPreviousMonth} title={textContent.previousMonthTitle || "Mois précédent"} aria-label={textContent.previousMonthTitle || "Mois précédent"}>{textContent.previousShortLabel || "Prec."}</button>
<strong>{formatMonthTitle(monthStart)}</strong>
<button type="button" onClick={onCurrentMonth}>{textContent.currentMonthTitle || "Mois courant"}</button>
<button type="button" onClick={onNextMonth} title={textContent.nextMonthTitle || "Mois suivant"} aria-label={textContent.nextMonthTitle || "Mois suivant"}>{textContent.nextShortLabel || "Suiv."}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onCurrentMonth}>{textContent.currentMonthTitle || "Mois courant"}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onNextMonth} title={textContent.nextMonthTitle || "Mois suivant"} aria-label={textContent.nextMonthTitle || "Mois suivant"}>{textContent.nextShortLabel || "Suiv."}</button>
</div>
<CalendarMonthGrid
days={monthGridDays}
@ -268,22 +277,29 @@ function CalendarMonthBar({ segment, active, onEdit }) {
const startColumn = segment.startIndex % WEEKDAY_LABELS.length + 1;
const endColumn = segment.endIndex % WEEKDAY_LABELS.length + 2;
const laneCount = Math.max(1, Math.min(3, segment.laneCount || 1));
const barHeight = laneCount === 1 ? 28 : laneCount === 2 ? 23 : 18;
const barGap = laneCount === 3 ? 3 : 4;
const barHeight = laneCount <= 2 ? 22 : 10;
const barGap = laneCount === 3 ? 2 : 4;
const eventSurfaceSize = barHeight <= 10 ? "xs" : barHeight <= 22 ? "sm" : "md";
const startLabel = new Date(segment.occurrence.startMs).toLocaleDateString(undefined, { day: "2-digit", month: "short", year: "numeric" });
const endDate = addDays(new Date(segment.occurrence.endMs), -1);
const endLabel = endDate.toLocaleDateString(undefined, { day: "2-digit", month: "short", year: "numeric" });
const dateLabel = startLabel === endLabel ? startLabel : `${startLabel} - ${endLabel}`;
const color = segment.occurrence.event.color || "violet";
return (
<article
className={`calendar-month-bar calendar-event-${segment.occurrence.event.color || "violet"} ${active ? "is-active" : ""}`}
className={`calendar-month-bar nds-event-surface ${active ? "is-active" : ""}`}
data-color={getCalendarColorValue(color)}
data-size={eventSurfaceSize}
data-variant={getCalendarEventSurfaceVariant(color)}
style={{
gridColumn: `${startColumn} / ${endColumn}`,
gridRow,
width: "auto",
"--calendar-event-lane": segment.lane || 0,
"--calendar-month-bar-height": `${barHeight}px`,
"--calendar-month-bar-gap": `${barGap}px`
"--calendar-month-bar-gap": `${barGap}px`,
"--nds-event-color": getCalendarColorValue(color)
}}
>
<Tooltip

View file

@ -113,10 +113,10 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
{editing && (
<div className="module-add-panel">
<form className="inline-form checklist-add-form" onSubmit={addItem}>
<input name="section" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} />
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" name="section" type="text" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} />
<input className="nds-input" data-size="sm" name="label" type="text" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
<input className="checklist-qty-input nds-input" data-size="sm" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -126,25 +126,27 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
</div>
)}
{namedSections.length > 0 && (
<div className="checklist-toolbar" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
{[
{ mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" },
{ mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" },
{ mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" }
].map((option) => (
<button
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
type="button"
role="radio"
aria-checked={completedSectionsMode === option.mode}
aria-label={option.label}
key={option.mode}
onClick={() => setCompletedSectionsMode(option.mode)}
title={option.label}
>
<Icon name={option.icon} />
</button>
))}
<div className="checklist-toolbar">
<div className="checklist-complete-mode-group nds-radio-button-group" data-size="xs" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
{[
{ mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" },
{ mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" },
{ mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" }
].map((option) => (
<button
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
type="button"
role="radio"
aria-checked={completedSectionsMode === option.mode}
aria-label={option.label}
key={option.mode}
onClick={() => setCompletedSectionsMode(option.mode)}
title={option.label}
>
<Icon name={option.icon} />
</button>
))}
</div>
</div>
)}
<ul className="checklist">

View file

@ -67,7 +67,9 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
{showTitle && (
<div className="checklist-section-header">
<button
className="checklist-drag-handle"
className="checklist-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: section.id })}
aria-label={`${textContent.reorderSectionTitle || "Déplacer la catégorie"} ${section.title}`}
@ -79,6 +81,7 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
<input
{...titleEdit.getInputProps({
className: "checklist-section-title-input",
type: "text",
title: textContent.sectionEditTitle || "Renommer la catégorie",
"aria-label": textContent.sectionEditTitle || "Renommer la catégorie"
})}
@ -87,7 +90,8 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
<div className="checklist-section-actions">
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={toggleSectionCollapsed}
aria-expanded={!isCollapsed}
@ -160,7 +164,9 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
<li className={className} {...reorder.getItemProps({ itemId: item.id, groupId: section.id })}>
<div className={`checklist-item-main ${editing ? "is-editing" : ""}`}>
<button
className="checklist-drag-handle"
className="checklist-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, item.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${item.label}`}
@ -170,17 +176,20 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
</button>
{item.qtyTarget === 1 ? (
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={done}
onChange={(event) => updateItem((entry) => ({ ...entry, qtyTarget: 1, qtyCurrent: event.target.checked ? 1 : 0 }))}
aria-label={`Terminer ${item.label}`}
/>
) : (
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
<div className="checklist-qty-controls nds-quantity-stepper" data-size="md" aria-label={`Quantité ${item.label}`}>
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label={textContent.decrementLabel || "Retirer une quantité"}>-</button>
<label className="checklist-qty-current">
<input
type="number"
className="nds-quantity-stepper-input"
max={item.qtyTarget}
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
@ -196,13 +205,15 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
<>
<input
{...labelEdit.getInputProps({
className: "checklist-label-input",
className: "checklist-label-input nds-inline-input",
type: "text",
"aria-label": textContent.itemEditLabel || "Libellé"
})}
/>
<input
{...categoryEdit.getInputProps({
className: "checklist-category-input",
className: "checklist-category-input nds-inline-input",
type: "text",
placeholder: textContent.sectionPlaceholder || "Catégorie",
"aria-label": textContent.categoryEditLabel || "Catégorie"
})}
@ -214,10 +225,10 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
</button>
)}
</div>
<button type="button" className="checklist-edit-button" onClick={onToggleEditing} aria-label={`${textContent.editTitle || "Modifier"} ${item.label}`} title={textContent.editTitle || "Modifier"}>
<button type="button" className="checklist-edit-button nds-icon-button" data-size="sm" onClick={onToggleEditing} aria-label={`${textContent.editTitle || "Modifier"} ${item.label}`} title={textContent.editTitle || "Modifier"}>
<Icon name="edit" />
</button>
<button className="checklist-delete-button danger" onClick={() => onDeleteItem(item.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${item.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="checklist-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={() => onDeleteItem(item.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${item.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</li>

View file

@ -48,14 +48,14 @@ export function CombosEditor({
<div className="combos-device-row">
<label>
<span>{textContent.deviceLabel || "Périphérique"}</span>
<select value={data.device} onChange={(event) => onDeviceChange(event.target.value)}>
<select className="nds-select" data-size="sm" value={data.device} onChange={(event) => onDeviceChange(event.target.value)}>
{DEVICE_OPTIONS.map((device) => (
<option key={device.value} value={device.value}>{device.label}</option>
))}
</select>
</label>
{data.device === "keyboardMouse" && (
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
<div className="combos-keyboard-layout-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
{[
{ value: "azerty", label: "AZERTY" },
{ value: "qwerty", label: "QWERTY" }
@ -78,11 +78,11 @@ export function CombosEditor({
<div className="combos-form-grid">
<label>
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
<input value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
<input className="nds-input" data-size="sm" type="text" value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
</label>
<label>
<span>{textContent.comboNameLabel || "Nom"}</span>
<input value={comboName} onChange={(event) => onComboNameChange(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
<input className="nds-input" data-size="sm" type="text" value={comboName} onChange={(event) => onComboNameChange(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
</label>
</div>
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
@ -115,8 +115,8 @@ export function CombosEditor({
{data.device === "keyboardMouse" && (
<div className="inline-form combos-custom-key-form">
<input value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button type="button" onClick={onCustomKeyAdd}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
<input className="nds-input" data-size="sm" type="text" value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button className="nds-button" data-size="sm" type="button" onClick={onCustomKeyAdd}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
</div>
)}
@ -135,7 +135,7 @@ export function CombosEditor({
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<span className="combo-hold-controls nds-compound-toggle-field">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
@ -146,6 +146,7 @@ export function CombosEditor({
{textContent.holdToggleLabel || "Hold"}
</button>
<input
className="nds-compound-toggle-field-input"
type="number"
min="0"
max="99"
@ -161,7 +162,8 @@ export function CombosEditor({
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
className={`combos-simultaneous-toggle nds-radio-button-group ${simultaneousMode ? "active" : ""}`}
data-size="xs"
onClick={onSimultaneousModeToggle}
aria-pressed={simultaneousMode}
title={textContent.simultaneousModeTitle || "Ajout simultané"}
@ -170,19 +172,19 @@ export function CombosEditor({
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
</button>
<button type="button" className="notepad-toolbar-button" onClick={onLastInputRemove} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<button type="button" className="notepad-toolbar-button nds-icon-button" data-size="sm" onClick={onLastInputRemove} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<Icon name="back" />
</button>
<button type="button" className="notepad-toolbar-button danger" onClick={onActiveStepRemove} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<button type="button" className="notepad-toolbar-button nds-icon-button" data-size="sm" data-variant="danger" onClick={onActiveStepRemove} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<Icon name="trash" />
</button>
<div className="combo-save-actions">
{editingComboId && (
<button type="button" className="combo-cancel-button" onClick={onComboEditCancel}>
<button type="button" className="combo-cancel-button nds-button" data-size="sm" onClick={onComboEditCancel}>
{textContent.cancelEditButton || "Annuler"}
</button>
)}
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
<button className="nds-button" data-size="sm" data-variant="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
</button>
</div>

View file

@ -27,7 +27,9 @@ export function CombosList({
return (
<article className={comboClassName} key={combo.id} {...reorder.getItemProps({ itemId: combo.id, groupId: category })}>
<button
className="task-planner-drag-handle combo-drag-handle"
className="task-planner-drag-handle combo-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, combo.id)}
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
@ -45,10 +47,10 @@ export function CombosList({
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
</div>
<div className="combo-card-actions">
<button type="button" className="checklist-delete-button" onClick={() => onComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
<button type="button" className="checklist-delete-button nds-icon-button" data-size="sm" onClick={() => onComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
<Icon name="edit" />
</button>
<button type="button" className="checklist-delete-button danger" onClick={() => onComboDelete(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
<button type="button" className="checklist-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={() => onComboDelete(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
<Icon name="trash" />
</button>
</div>
@ -88,7 +90,9 @@ export function CombosList({
<div className="checklist-section-header combos-category-header">
<div className="combos-category-title">
<button
className="task-planner-category-drag-handle"
className="task-planner-category-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })}
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
@ -101,7 +105,8 @@ export function CombosList({
<div className="checklist-section-actions">
<span>{group.combos.length}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onCategoryToggle(category)}
aria-expanded={!isCollapsed}

View file

@ -83,7 +83,8 @@ export function InlineTextInput({ value, onCommit, className, ariaLabel, placeho
return (
<input
{...inlineEdit.getInputProps({
className,
className: `${className} nds-inline-input`.trim(),
type: "text",
placeholder,
"aria-label": ariaLabel
})}

View file

@ -47,8 +47,8 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
{editing && (
<div className="module-add-panel">
<form className="inline-form counters-add-form" onSubmit={addCounter}>
<input {...counterDraft.getFieldProps("label", { placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input {...counterDraft.getFieldProps("label", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -69,7 +69,9 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
return (
<article className={className} key={counter.id} {...getItemProps({ itemId: counter.id, parentId: moduleId })}>
<button
className="counter-drag-handle"
className="counter-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, counter.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${counter.label}`}
@ -82,12 +84,12 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
<span>{counter.label}</span>
</div>
<div className="counter-actions">
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`${textContent.decrementLabel || "Décrémenter"} ${counter.label}`}>-</button>
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`${textContent.incrementLabel || "Incrémenter"} ${counter.label}`}>+</button>
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`${textContent.resetTitle || "Réinitialiser"} ${counter.label}`} title={textContent.resetTitle || "Réinitialiser"}>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`${textContent.decrementLabel || "Décrémenter"} ${counter.label}`}>-</button>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`${textContent.incrementLabel || "Incrémenter"} ${counter.label}`}>+</button>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`${textContent.resetTitle || "Réinitialiser"} ${counter.label}`} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
<button type="button" className="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${counter.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="sm" data-variant="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${counter.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -7,13 +7,14 @@ const DEFAULT_WIDTHS = [2, 4, 8, 12];
export function DrawingControls({ textContent = {}, active, onToggleActive, eraseActive, onToggleErase, color, colors, onColorChange, width, widths = DEFAULT_WIDTHS, onWidthChange, onUndo, onClear, storageMode, onToggleStorageMode }) {
return (
<div className={`drawing-controls-panel ${active ? "is-open" : ""}`} aria-label={textContent.drawingLabel || "Dessin"}>
<button className={`drawing-control-button ${active ? "active" : ""}`} type="button" aria-pressed={active} title={textContent.drawTitle || "Dessiner"} onClick={onToggleActive}>
<button className={`drawing-control-button nds-icon-button ${active ? "active" : ""}`} data-size="sm" type="button" aria-pressed={active} title={textContent.drawTitle || "Dessiner"} onClick={onToggleActive}>
<Icon name="edit" />
</button>
{active && (
<>
<button
className={`drawing-control-button ${eraseActive ? "active" : ""}`}
className={`drawing-control-button nds-icon-button ${eraseActive ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={eraseActive}
title={textContent.eraseTitle || "Gommer"}
@ -33,15 +34,16 @@ export function DrawingControls({ textContent = {}, active, onToggleActive, eras
value={width}
onSelect={onWidthChange}
/>
<button className="drawing-control-button" type="button" title={textContent.undoDrawingTitle || "Annuler le dernier trait"} onClick={onUndo}>
<button className="drawing-control-button nds-icon-button" data-size="sm" type="button" title={textContent.undoDrawingTitle || "Annuler le dernier trait"} onClick={onUndo}>
<Icon name="back" />
</button>
<button className="drawing-control-button danger" type="button" title={textContent.clearDrawingTitle || "Effacer les dessins"} onClick={onClear}>
<button className="drawing-control-button nds-icon-button" data-size="sm" data-variant="danger" type="button" title={textContent.clearDrawingTitle || "Effacer les dessins"} onClick={onClear}>
<Icon name="trash" />
</button>
{onToggleStorageMode && (
<button
className={`module-scroll-button drawing-storage-switch ${storageMode === "permanent" ? "active" : ""}`}
className={`module-scroll-button nds-switch drawing-storage-switch ${storageMode === "permanent" ? "active" : ""}`}
data-size="lg"
type="button"
aria-pressed={storageMode === "permanent"}
aria-label={storageMode === "permanent" ? textContent.permanentTitle || "Annotations permanentes" : textContent.temporaryTitle || "Annotations temporaires"}
@ -67,7 +69,7 @@ export function FormattingColorDropdown({ label, sections }) {
closeOnMouseLeave
preventMouseDown
renderTrigger={({ toggle }) => (
<button className="notepad-color-toggle is-rainbow" type="button" title={label} aria-label={label} onClick={toggle} />
<button className="notepad-color-toggle nds-swatch is-rainbow" data-variant="rainbow" type="button" title={label} aria-label={label} onClick={toggle} />
)}
>
{({ close }) => (
@ -79,7 +81,7 @@ export function FormattingColorDropdown({ label, sections }) {
{section.colors.map((color) => (
<button
key={`${section.label}-${color}`}
className={`notepad-swatch ${section.value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${section.value === color ? "active" : ""}`}
type="button"
title={`${section.label} ${color}`}
aria-label={`${section.label} ${color}`}
@ -110,7 +112,8 @@ export function DrawingColorDropdown({ label, colors, value, onSelect }) {
preventMouseDown
renderTrigger={({ toggle }) => (
<button
className="notepad-color-toggle is-rainbow"
className="notepad-color-toggle nds-swatch is-rainbow"
data-variant="rainbow"
type="button"
title={label}
aria-label={label}
@ -124,7 +127,7 @@ export function DrawingColorDropdown({ label, colors, value, onSelect }) {
{colors.map((color) => (
<button
key={color}
className={`notepad-swatch ${value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${value === color ? "active" : ""}`}
type="button"
title={`${label} ${color}`}
aria-label={`${label} ${color}`}
@ -151,7 +154,7 @@ export function DrawingWidthDropdown({ label, widths, value, onSelect }) {
closeOnMouseLeave
preventMouseDown
renderTrigger={({ toggle }) => (
<button className="drawing-width-toggle" type="button" title={label} aria-label={label} onClick={toggle}>
<button className="drawing-width-toggle nds-icon-button" data-size="sm" type="button" title={label} aria-label={label} onClick={toggle}>
<span style={{ "--line-width": `${value}px` }} />
</button>
)}
@ -161,7 +164,8 @@ export function DrawingWidthDropdown({ label, widths, value, onSelect }) {
{widths.map((lineWidth) => (
<button
key={lineWidth}
className={`drawing-width-button ${value === lineWidth ? "active" : ""}`}
className={`drawing-width-button nds-icon-button ${value === lineWidth ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={value === lineWidth}
title={`${label} ${lineWidth}`}

View file

@ -81,10 +81,10 @@ export function CharacteristicsEditor({ equipment, textContent, editing, onUpdat
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
/>
<input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
<input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button>{textContent.addTraitButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
<input className="nds-input" data-size="sm" type="text" value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addTraitButton || "Ajouter"}</button>
</form>
)}
<TraitCategoryList
@ -145,12 +145,12 @@ function TraitCategoryList({ traits, categories, collapsedCategories, reorder, t
return (
<section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
<div className="equipment-planner-trait-category-header">
<button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
<button type="button" className="task-planner-category-drag-handle nds-icon-button" data-size="xs" data-variant="ghost" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
<Icon name="drag" />
</button>
<strong>{category}</strong>
<span>{categoryTraits.length}</span>
<button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
<button type="button" className="checklist-section-collapse-button nds-icon-button" data-size="xs" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
</button>
</div>
@ -182,7 +182,7 @@ function EquipmentTraitRow({ trait, className, itemProps, editing, textContent,
<li className={className} {...itemProps}>
{editing ? (
<>
<button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
<button type="button" className="task-planner-drag-handle nds-icon-button" data-size="xs" data-variant="ghost" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
<Icon name="drag" />
</button>
<IconChoiceDropdown
@ -191,10 +191,10 @@ function EquipmentTraitRow({ trait, className, itemProps, editing, textContent,
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
/>
<input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
<input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
<input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
<input {...categoryEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.traitNamePlaceholder || "Nom" })} />
<input {...valueEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>

View file

@ -30,7 +30,9 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<li className={className} {...reorder.getItemProps({ itemId: equipment.id })}>
<div className="equipment-planner-line">
<button
className="task-planner-drag-handle"
className="task-planner-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, equipment.id)}
aria-label={`${textContent.reorderEquipmentTitle || "Déplacer"} ${equipment.name}`}
@ -42,9 +44,9 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<span className="equipment-planner-type-icon" aria-hidden="true">
<Icon name={equipment.icon || "shield"} />
</span>
<strong {...nameEdit.getContentEditableProps({ className: "equipment-planner-name", title: textContent.renameEquipmentTitle || "Renommer" })}>{equipment.name}</strong>
<strong {...nameEdit.getContentEditableProps({ className: "equipment-planner-name nds-inline-input", title: textContent.renameEquipmentTitle || "Renommer" })}>{equipment.name}</strong>
</div>
<label className="equipment-planner-include-switch">
<label className="equipment-planner-include-switch nds-native-switch">
<input
type="checkbox"
checked={equipment.active}
@ -55,21 +57,22 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
</label>
<button
type="button"
className={`task-planner-settings-button equipment-planner-row-edit-button ${detailEditing ? "active" : ""}`}
className={`task-planner-settings-button equipment-planner-row-edit-button nds-icon-button ${detailEditing ? "active" : ""}`}
data-size="sm"
onClick={() => setDetailEditing((value) => !value)}
aria-pressed={detailEditing}
title={detailEditing ? textContent.viewModeTitle || "Mode affichage" : textContent.editModeTitle || "Mode édition"}
>
<Icon name="edit" />
</button>
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} aria-label={`${textContent.deleteEquipmentTitle || "Supprimer"} ${equipment.name}`} title={textContent.deleteEquipmentTitle || "Supprimer"}>
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={onDelete} aria-label={`${textContent.deleteEquipmentTitle || "Supprimer"} ${equipment.name}`} title={textContent.deleteEquipmentTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
<div className="equipment-planner-details">
{detailEditing ? (
<>
<div className="equipment-planner-tabs" role="tablist" aria-label={textContent.tabsLabel || "Ajout de caractéristiques"}>
<div className="equipment-planner-tabs nds-radio-button-group" data-size="sm" role="tablist" aria-label={textContent.tabsLabel || "Ajout de caractéristiques"}>
{DETAIL_TABS.map((tab) => (
<button
key={tab}
@ -91,7 +94,7 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<section className="equipment-planner-craft-tab">
<label className="equipment-planner-field">
<span>{textContent.obtainLabel || "Obtention"}</span>
<textarea {...obtainEdit.getInputProps({ placeholder: textContent.obtainPlaceholder || "Comment obtenir ou créer cet équipement" })} />
<textarea {...obtainEdit.getInputProps({ className: "nds-textarea", "data-size": "sm", placeholder: textContent.obtainPlaceholder || "Comment obtenir ou créer cet équipement" })} />
</label>
<MaterialsEditor equipment={equipment} textContent={textContent} editing onUpdate={onUpdate} />
</section>

View file

@ -23,9 +23,9 @@ export function MaterialsEditor({ equipment, textContent, editing, onUpdate }) {
<h4>{textContent.materialsTitle || "Matériaux"}</h4>
{editing && (
<form className="inline-form equipment-planner-material-form" onSubmit={addMaterial}>
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
<input type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
<button>{textContent.addMaterialButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
<input className="nds-input" data-size="sm" type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addMaterialButton || "Ajouter"}</button>
</form>
)}
<ul className="equipment-planner-materials">
@ -54,9 +54,9 @@ function MaterialRow({ material, editing, textContent, onUpdate }) {
<li>
{editing ? (
<>
<input {...nameEdit.getInputProps()} />
<input type="number" min="1" value={material.qty} onChange={(event) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text" })} />
<input className="nds-input" data-size="xs" type="number" min="1" value={material.qty} onChange={(event) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>

View file

@ -109,8 +109,8 @@ export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing,
options={EQUIPMENT_ICONS}
onChange={setEquipmentIcon}
/>
<input value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
<button className="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
</form>
</div>
)}

View file

@ -21,7 +21,9 @@ export function IconChoiceDropdown({ className = "", label, value, options, onCh
columns={Math.min(options.length, 5)}
renderTrigger={({ open, toggle }) => (
<button
className={`equipment-planner-picker-trigger ${open ? "active" : ""}`}
className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`}
data-size="sm"
data-variant="icon-picker"
type="button"
onClick={toggle}
aria-label={label}
@ -29,7 +31,6 @@ export function IconChoiceDropdown({ className = "", label, value, options, onCh
title={selected.label}
>
<Icon name={selected.icon} />
<Icon name="dropdown" />
</button>
)}
>
@ -63,9 +64,8 @@ export function ShapeChoiceDropdown({ label, value, onChange }) {
label={label}
columns={SOCKET_SHAPES.length}
renderTrigger={({ open, toggle }) => (
<button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<button className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`} data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<SocketShapeIcon shape={value} />
<Icon name="dropdown" />
</button>
)}
>
@ -98,9 +98,8 @@ export function ColorChoiceDropdown({ label, value, onChange }) {
label={label}
columns={6}
renderTrigger={({ open, toggle }) => (
<button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={value}>
<button className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`} data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={value}>
<span className={`equipment-planner-color-swatch color-${value}`} aria-hidden="true" />
<Icon name="dropdown" />
</button>
)}
>

View file

@ -53,10 +53,10 @@ export function SocketItemsEditor({ equipment, textContent, editing, onUpdate })
<h4>{textContent.socketItemsTitle || "Objets sertis"}</h4>
{editing && (
<form className="inline-form equipment-planner-socket-form" onSubmit={addSocketItem}>
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={draft.shape} onChange={(shape) => setDraft((current) => ({ ...current, shape }))} />
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={draft.color} onChange={(color) => setDraft((current) => ({ ...current, color }))} />
<button>{textContent.addSocketButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addSocketButton || "Ajouter"}</button>
</form>
)}
<div className="equipment-planner-socket-grid">
@ -104,8 +104,8 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
{editing ? (
<>
<input {...nameEdit.getInputProps()} />
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text" })} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>
@ -119,16 +119,16 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={socketItem.shape} onChange={(shape) => onUpdate((current) => ({ ...current, shape }))} />
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={socketItem.color} onChange={(color) => onUpdate((current) => ({ ...current, color }))} />
{equipment.socketItems.length > 1 && (
<select defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
<select className="nds-select" data-size="xs" defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
<option value="">{textContent.addSocketLinkLabel || "Lier à"}</option>
{equipment.socketItems.filter((item) => item.id !== socketItem.id).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
)}
</div>
<form className="inline-form equipment-planner-bonus-form" onSubmit={addBonus}>
<input value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
<input value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
<input className="nds-input" data-size="sm" type="text" value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
<input className="nds-input" data-size="sm" type="text" value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button className="nds-button" data-size="sm" data-variant="primary" aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
</form>
</>
)}
@ -138,7 +138,7 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
<b>{formatValue(bonus.value)}</b>
{editing && (
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
<Icon name="trash" />
</button>
)}

View file

@ -49,17 +49,19 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
<article className={`goal-tree-node-card ${editing ? "is-editing" : "is-viewing"} ${hasNodeActions ? "has-actions" : ""}`} data-diagram-node={node.id} onClick={selectFromCard}>
{!editing && (
<div className="goal-tree-node-hover-actions" aria-label={textContent.cardActionsLabel || "Actions de l'objectif"}>
<button type="button" onClick={() => onEditNode(node.id)} aria-label={`${textContent.editTitle || "Modifier"} ${node.title}`} title={textContent.editTitle || "Modifier"}>
<button className="nds-icon-button" data-size="tiny" type="button" onClick={() => onEditNode(node.id)} aria-label={`${textContent.editTitle || "Modifier"} ${node.title}`} title={textContent.editTitle || "Modifier"}>
<Icon name="edit" />
</button>
<button className="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="tiny" data-variant="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
)}
{editing && (
<button
className="goal-tree-drag-handle"
className="goal-tree-drag-handle nds-icon-button"
data-size="micro"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, node.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${node.title}`}
@ -109,13 +111,14 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
{hasNodeActions && (
<div className="goal-tree-node-actions">
{editing && (
<button type="button" onClick={() => onAdd(node.id)} aria-label={`${textContent.addChildTitle || "Ajouter une sous-étape"} ${node.title}`} title={textContent.addChildTitle || "Ajouter une sous-étape"}>
<button className="nds-icon-button" data-size="micro" type="button" onClick={() => onAdd(node.id)} aria-label={`${textContent.addChildTitle || "Ajouter une sous-étape"} ${node.title}`} title={textContent.addChildTitle || "Ajouter une sous-étape"}>
<Icon name="add" />
</button>
)}
{hasBranchToggle && (
<button
className="goal-tree-branch-toggle"
className="goal-tree-branch-toggle nds-icon-button"
data-size="micro"
type="button"
onClick={() => onToggleBranch(node.id)}
aria-expanded={!branchCollapsed}
@ -126,7 +129,7 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
</button>
)}
{editing && (
<button className="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
)}

View file

@ -94,11 +94,11 @@ export function GoalTreeEditor({
)}
<div className="goal-tree-form-actions">
<button type="button" onClick={onClose}>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>
<Icon name="close" />
<span>{textContent.closeButton || "Fermer"}</span>
</button>
<button className="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
<button className="nds-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
<Icon name="trash" />
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
</button>
@ -113,7 +113,7 @@ function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSe
<div className="goal-tree-form-panel">
<label>
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
<select value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
<select className="nds-select" data-size="sm" value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
</select>
</label>
@ -139,7 +139,7 @@ function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSe
{GOAL_TREE_COLORS.map((color) => (
<button
key={color}
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
className={`goal-tree-color-swatch nds-swatch ${selectedNode.color === color ? "active" : ""}`}
type="button"
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, color }))}
aria-pressed={selectedNode.color === color}
@ -164,20 +164,20 @@ function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgr
<div className="goal-tree-form-panel">
<label>
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
<select value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
<select className="nds-select" data-size="sm" value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
</select>
</label>
{selectedNode.resetType !== "none" && (
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
<input className="nds-time" data-size="sm" type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
</label>
)}
{selectedNode.resetType === "weekly" && (
<label>
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
<select className="nds-select" data-size="sm" value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
@ -199,7 +199,8 @@ function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgr
</label>
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
<button
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle nds-switch ${selectedProgressMode === "auto" ? "active" : ""}`}
data-size="lg"
type="button"
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
aria-pressed={selectedProgressMode === "auto"}

View file

@ -22,13 +22,13 @@ export function GoalTreePrerequisitePicker({ nodes, node, textContent, onToggle
<div className="goal-tree-prerequisite-list themed-scrollbar">
{primaryParent && (
<label className="goal-tree-prerequisite-item is-primary">
<input type="checkbox" checked disabled readOnly />
<input className="nds-checkbox" data-size="xs" type="checkbox" checked disabled readOnly />
<span>{primaryParent.title}</span>
</label>
)}
{options.map((candidate) => (
<label className="goal-tree-prerequisite-item" key={candidate.id}>
<input type="checkbox" checked={parentIds.has(candidate.id)} onChange={() => onToggle(candidate.id)} />
<input className="nds-checkbox" data-size="xs" type="checkbox" checked={parentIds.has(candidate.id)} onChange={() => onToggle(candidate.id)} />
<span>{candidate.title}</span>
</label>
))}
@ -41,6 +41,7 @@ export function GoalTreeTitleInput({ value, onChange }) {
const [draft, setDraft] = useState(value || "");
return (
<input
type="text"
value={draft}
onChange={(event) => {
const title = event.target.value;
@ -54,7 +55,7 @@ export function GoalTreeTitleInput({ value, onChange }) {
export function GoalTreeFormulaInput({ value, onChange, hasParent, textContent, placeholder, title, className = "", "aria-label": ariaLabel }) {
const normalizedValue = String(value ?? "");
if (!hasParent) {
return <input aria-label={ariaLabel} className={className} inputMode="decimal" onChange={(event) => onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />;
return <input type="text" aria-label={ariaLabel} className={`${className} nds-inline-input`.trim()} inputMode="decimal" onChange={(event) => onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />;
}
return (
@ -87,7 +88,7 @@ export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDe
<div className="goal-tree-form-section-title">{textContent.contentSectionTitle || "Contenu"}</div>
{(node.items || []).map((item) => (
<div className={`goal-tree-content-editor-row is-${node.kind}`} key={item.id}>
<input className="goal-tree-content-label-input" value={item.label} placeholder={textContent.itemLabelPlaceholder || "Élément"} onChange={(event) => onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} />
<input className="goal-tree-content-label-input nds-input" data-size="sm" type="text" value={item.label} placeholder={textContent.itemLabelPlaceholder || "Élément"} onChange={(event) => onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} />
{node.kind === "checklist" && (
<GoalTreeFormulaInput
aria-label={textContent.itemTotalLabel || "Total"}
@ -100,12 +101,12 @@ export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDe
title={textContent.itemTotalHelp || "Saisissez un nombre ou une formule avec base pour référencer la cible du parent."}
/>
)}
<button className="danger" type="button" onClick={() => onDelete(item.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(item.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
))}
<button className="goal-tree-content-add-button" type="button" onClick={onAdd}>
<button className="goal-tree-content-add-button nds-button" data-size="xs" data-variant="dashed" type="button" onClick={onAdd}>
<Icon name="add" />
<span>{textContent.addContentItemButton || "Ajouter une ligne"}</span>
</button>
@ -122,12 +123,9 @@ export function GoalTreeIconPicker({ label, value, textContent, onChange }) {
menuClassName="goal-tree-icon-picker-menu"
label={label}
renderTrigger={({ open, toggle }) => (
<button className="goal-tree-icon-picker-button" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<span className="goal-tree-icon-preview" aria-hidden="true">
<Icon name={value} />
</span>
<button className="goal-tree-icon-picker-button nds-select-trigger" data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<Icon name={value} />
<span>{textContent[`icon${value}`] || value}</span>
<Icon name="dropdown" />
</button>
)}
>
@ -136,7 +134,8 @@ export function GoalTreeIconPicker({ label, value, textContent, onChange }) {
{GOAL_TREE_ICON_NAMES.map((icon) => (
<button
key={icon}
className={icon === value ? "active" : ""}
className={`nds-icon-button ${icon === value ? "active" : ""}`.trim()}
data-size="sm"
type="button"
onClick={() => {
onChange(icon);

View file

@ -7,10 +7,10 @@ function GoalTreeChecklistInlineControl({ item, textContent, onUpdate }) {
const target = Math.max(1, item.qtyTarget || 1);
const current = Math.min(Math.max(0, item.qtyCurrent || 0), target);
if (target <= 1) {
return <input className="goal-tree-node-check" type="checkbox" checked={current >= 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />;
return <input className="goal-tree-node-check nds-checkbox" data-size="xs" type="checkbox" checked={current >= 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />;
}
return (
<span className="goal-tree-content-qty-stepper">
<span className="goal-tree-content-qty-stepper nds-quantity-stepper" data-size="sm">
<button type="button" onClick={() => onUpdate((entry) => ({ ...entry, qtyCurrent: Math.max(0, (entry.qtyCurrent || 0) - 1) }))} aria-label={textContent.decrementItemTitle || "Retirer"} title={textContent.decrementItemTitle || "Retirer"}>
<span aria-hidden="true">-</span>
</button>
@ -26,10 +26,10 @@ export function GoalTreeStepLeafControl({ node, textContent, onChange }) {
const target = Math.max(1, node.computedQtyTarget || node.qtyTarget || 1);
const current = Math.min(Math.max(0, node.qtyCurrent || 0), target);
if (target <= 1) {
return <input className="goal-tree-step-check" type="checkbox" checked={current >= 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />;
return <input className="goal-tree-step-check nds-checkbox" data-size="xs" type="checkbox" checked={current >= 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />;
}
return (
<span className="goal-tree-step-qty-stepper goal-tree-content-qty-stepper">
<span className="goal-tree-step-qty-stepper goal-tree-content-qty-stepper nds-quantity-stepper" data-size="xs">
<button type="button" onClick={() => onChange(Math.max(0, current - 1))} aria-label={textContent.decrementItemTitle || "Retirer"} title={textContent.decrementItemTitle || "Retirer"}>
<span aria-hidden="true">-</span>
</button>

View file

@ -87,7 +87,8 @@ export function GoalTreeView({
return (
<section className={`goal-tree-view tool-split-results ${verticalView ? "is-vertical" : "is-horizontal"} ${editing ? "is-editing" : ""}`} aria-label={textContent.treeTitle || "Arbre d'objectifs"}>
<button
className={`module-scroll-button goal-tree-orientation-switch ${verticalView ? "active" : ""}`}
className={`module-scroll-button goal-tree-orientation-switch nds-switch ${verticalView ? "active" : ""}`}
data-size="lg"
type="button"
onClick={onToggleOrientation}
aria-label={verticalView ? textContent.horizontalViewTitle || "Afficher horizontalement" : textContent.verticalViewTitle || "Afficher verticalement"}
@ -100,7 +101,7 @@ export function GoalTreeView({
<div className="goal-tree-scroll-area themed-scrollbar">
<div className="goal-tree-view-controls">
{editing && (
<button className="goal-tree-add-root-button" type="button" onClick={() => onAdd("")}>
<button className="goal-tree-add-root-button nds-button" data-size="sm" data-variant="dashed" type="button" onClick={() => onAdd("")}>
<Icon name="add" />
<span>{textContent.addRootSchemaButton || "Ajouter un objectif a la racine"}</span>
</button>

View file

@ -123,7 +123,8 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
<img src={data.image} alt={textContent.imageAlt || "Image annotée"} draggable="false" />
<div className="annotation-preview-actions">
<button
className="tool-split-action-button"
className="tool-split-action-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => context.setImage({
dataUrl: data.image,
@ -164,7 +165,7 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
<Icon name="zoom" />
</button>
{editing && (
<button className="tool-split-action-button danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
<Icon name="trash" />
</button>
)}

View file

@ -104,7 +104,9 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
return (
<figure key={image.id} className={className} {...getItemProps({ itemId: image.id, parentId: moduleId })}>
<button
className="image-drag-handle"
className="image-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, image.id)}
aria-label={textContent.reorderAriaLabel || "Déplacer l'image"}
@ -117,7 +119,8 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
</button>
{editing ? (
<input
className="image-label-input"
className="image-label-input nds-inline-input"
type="text"
value={image.label || ""}
placeholder={textContent.labelPlaceholder || "Libellé de l'image"}
onChange={(event) => updateImage(image.id, { label: event.target.value })}
@ -128,7 +131,8 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
) : null}
<div className="image-actions">
<button
className="image-annotate-button"
className="image-annotate-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => context.createImageAnnotationModule?.(image.dataUrl)}
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
@ -136,7 +140,7 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
>
<Icon name="map" />
</button>
<button className="image-delete-button danger" type="button" onClick={() => {
<button className="image-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => {
context.setModuleData(toolboxId, moduleId, { images: data.images.filter((item) => item.id !== image.id) });
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />

View file

@ -59,9 +59,9 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
{editing && (
<div className="module-add-panel">
<form className="inline-form links-add-form" onSubmit={addLink}>
<input {...linkDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input {...linkDraft.getFieldProps("title", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
<input {...linkDraft.getFieldProps("url", { className: "nds-input", "data-size": "sm", type: "url", placeholder: textContent.urlPlaceholder || "https://..." })} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -82,7 +82,9 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
return (
<li className={className} key={link.id} {...getItemProps({ itemId: link.id, parentId: moduleId })}>
<button
className="link-drag-handle"
className="link-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, link.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${link.title || link.url}`}
@ -95,10 +97,10 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
<span>{link.url}</span>
</a>
<div>
<button type="button" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
<button type="button" className="nds-icon-button" data-size="sm" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
<Icon name="copy" />
</button>
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="sm" data-variant="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -40,7 +40,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
function resizeMemoTextarea(element) {
if (!element) return;
element.style.height = "auto";
element.style.height = `${element.scrollHeight}px`;
element.style.height = `${element.scrollHeight + 2}px`;
}
function save(nextData) {
@ -93,7 +93,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
{editing && (
<div className="module-add-panel">
<button className="primary memos-add-button" type="button" onClick={addMemo}>
<button className="memos-add-button nds-button" data-size="sm" data-variant="primary" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.addButton || "Ajouter"}</span>
</button>
@ -102,7 +102,8 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
<div className="memos-toolbar">
<button
className={`memos-ticker-toggle ${data.tickerEnabled ? "active" : ""}`}
className={`memos-ticker-toggle nds-icon-button ${data.tickerEnabled ? "active" : ""}`}
data-size="sm"
type="button"
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
aria-pressed={data.tickerEnabled}
@ -135,7 +136,9 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
return (
<article className={className} key={memo.id} style={{ "--memo-color": memo.color }} {...getItemProps({ itemId: memo.id, parentId: moduleId })}>
<button
className="memo-drag-handle"
className="memo-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, memo.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${memo.text}`}
@ -144,6 +147,9 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
<Icon name="drag" />
</button>
<textarea
className="nds-textarea"
data-size="sm"
data-variant="ghost"
ref={(element) => {
if (element) {
textareaRefs.current.set(memo.id, element);
@ -169,7 +175,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
label={textContent.colorLabel || "Couleur du mémo"}
renderTrigger={({ toggle }) => (
<button
className="notepad-color-toggle"
className="notepad-color-toggle nds-swatch"
type="button"
style={{ "--swatch-color": memo.color }}
onClick={toggle}
@ -183,7 +189,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
{TEXT_COLORS.map((color) => (
<button
key={color}
className={`notepad-swatch ${memo.color === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${memo.color === color ? "active" : ""}`}
type="button"
style={{ "--swatch-color": color }}
onClick={() => {
@ -196,14 +202,14 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
</>
)}
</CompactDropdown>
<button className="memo-delete-button danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
<button className="memo-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</footer>
</article>
);
}) : (
<button className="memos-empty" type="button" onClick={addMemo}>
<button className="memos-empty nds-button" data-size="sm" data-variant="dashed" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.emptyLabel || "Ajouter un premier mémo"}</span>
</button>

View file

@ -295,7 +295,11 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
label={textContent.pagesMenuLabel || "Pages du bloc note"}
renderTrigger={({ open, toggle }) => (
<button
className={`notepad-pages-toggle ${open ? "active" : ""}`}
className={`notepad-pages-toggle nds-icon-button ${open ? "active" : ""}`}
data-size="sm"
data-variant="panel-toggle"
data-edge="right"
data-border="joined"
type="button"
onClick={toggle}
aria-label={textContent.pagesToggleLabel || "Afficher les pages du bloc note"}
@ -311,10 +315,12 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
<div className="notepad-pages-list">
{normalizedData.pages.map((page) => (
<div className={`notepad-page-row ${page.id === normalizedData.activePageId ? "active" : ""}`} key={page.id}>
<div className="notepad-page-main">
<div className="notepad-page-main nds-list-row">
{editingPageId === page.id ? (
<input
className="notepad-page-title-input"
className="notepad-page-title-input nds-input"
data-size="xs"
type="text"
value={pageTitleDrafts[page.id] ?? page.title}
aria-label={`${textContent.renamePageLabel || "Renommer"} ${page.title}`}
autoFocus
@ -339,7 +345,8 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
</div>
<div className="notepad-page-actions">
<button
className="notepad-page-edit"
className="notepad-page-edit nds-icon-button"
data-size="xxs"
type="button"
onClick={() => {
setPageTitleDrafts((current) => ({ ...current, [page.id]: page.title }));
@ -351,7 +358,9 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
<Icon name="edit" />
</button>
<button
className="notepad-page-delete danger"
className="notepad-page-delete nds-icon-button"
data-size="xxs"
data-variant="danger"
type="button"
onClick={() => deletePage(page.id, close)}
disabled={normalizedData.pages.length <= 1}
@ -364,7 +373,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
</div>
))}
</div>
<button className="notepad-page-add" type="button" onClick={() => addPage(close)}>
<button className="notepad-page-add nds-button" data-size="xs" data-variant="dashed" type="button" onClick={() => addPage(close)}>
<Icon name="add" />
<span>{textContent.addPageLabel || "Ajouter une page"}</span>
</button>

View file

@ -52,7 +52,8 @@ export function NotepadToolbar({
function ToolbarButton({ active, title, children, onClick }) {
return (
<button
className={`notepad-toolbar-button ${active ? "active" : ""}`}
className={`notepad-toolbar-button nds-icon-button ${active ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={active}
title={title}
@ -74,7 +75,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
preventMouseDown
renderTrigger={({ toggle }) => (
<button
className={`notepad-toolbar-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
className={`notepad-toolbar-button nds-icon-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.bulletList || activeFormats.numberList}
title={textContent.listGroupLabel || "Listes"}
@ -87,7 +89,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
{({ close }) => (
<>
<button
className={`notepad-list-option ${activeFormats.bulletList ? "active" : ""}`}
className={`notepad-list-option nds-icon-button ${activeFormats.bulletList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.bulletList}
onClick={() => {
@ -98,7 +101,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
•
</button>
<button
className={`notepad-list-option ${activeFormats.numberList ? "active" : ""}`}
className={`notepad-list-option nds-icon-button ${activeFormats.numberList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.numberList}
onClick={() => {

View file

@ -33,6 +33,7 @@ export function TableRow({ rowIndex, columns, cells, evaluatedCells, editingCell
return (
<input
type="text"
key={address}
ref={(element) => registerInput(address, element)}
className={className}
@ -82,6 +83,7 @@ export function HeaderLabelInput({ className, value, fallback, onSave, ...props
<input
{...inlineEdit.getInputProps({
...props,
type: "text",
className,
onFocus: (event) => {
if (value === fallback) window.requestAnimationFrame(() => event.target.select());

View file

@ -8,7 +8,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<div className="notepad-format-controls table-controls">
<div className="notepad-toolbar-group" role="group" aria-label={textContent.rowActionsLabel || "Lignes"}>
<button
className="notepad-toolbar-button"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => onResize(Math.min(MAX_ROWS, rows + 1), columns)}
disabled={rows >= MAX_ROWS}
@ -18,7 +19,9 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<Icon name="add-row" />
</button>
<button
className="notepad-toolbar-button danger"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={() => onResize(Math.max(1, rows - 1), columns)}
disabled={rows <= 1}
@ -30,7 +33,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
</div>
<div className="notepad-toolbar-group" role="group" aria-label={textContent.columnActionsLabel || "Colonnes"}>
<button
className="notepad-toolbar-button"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => onResize(rows, Math.min(MAX_COLUMNS, columns + 1))}
disabled={columns >= MAX_COLUMNS}
@ -40,7 +44,9 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<Icon name="add-column" />
</button>
<button
className="notepad-toolbar-button danger"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={() => onResize(rows, Math.max(1, columns - 1))}
disabled={columns <= 1}
@ -56,7 +62,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
</div>
<div className="notepad-toolbar-group table-copy-group">
<button
className="notepad-toolbar-button table-copy-button"
className="notepad-toolbar-button table-copy-button nds-button"
data-size="sm"
type="button"
onClick={onCopy}
aria-label={textContent.copyTitle || "Copier les valeurs"}

View file

@ -78,6 +78,8 @@ export function TaskPlannerBranch({
<li className="task-planner-hidden-parent" key={`${task.id}:visible-children`}>
<div className="task-planner-hidden-parent-line">
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={task.checked}
onChange={(event) => onUpdateTask(task.id, (current) => ({
@ -158,7 +160,9 @@ export function TaskPlannerBranch({
<div className="checklist-section-header task-planner-category-header">
<div className="task-planner-category-title">
<button
className="task-planner-category-drag-handle"
className="task-planner-category-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: group.category, parentId })}
aria-label={`${textContent.categoryReorderTitle || "Déplacer la catégorie"} ${group.category}`}
@ -171,7 +175,8 @@ export function TaskPlannerBranch({
<div className="task-planner-category-actions">
<span>{groupCompletedCount} / {groupCount}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onToggleCategoryCollapsed(group.category)}
aria-expanded={!isCollapsed}

View file

@ -65,7 +65,9 @@ export function TaskPlannerItem({
<li className={className} {...reorder.getItemProps({ itemId: task.id, groupId: getTaskCategoryDataset(task, tasks, parentMap), parentId })}>
<div className="task-planner-line">
<button
className="task-planner-drag-handle"
className="task-planner-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, task.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${task.title}`}
@ -74,6 +76,8 @@ export function TaskPlannerItem({
<Icon name="drag" />
</button>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={task.checked}
onChange={updateChecked}
@ -81,12 +85,14 @@ export function TaskPlannerItem({
/>
<input
{...titleEdit.getInputProps({
className: "tool-split-entry-label task-planner-title-input",
className: "tool-split-entry-label task-planner-title-input nds-inline-input",
type: "text",
"aria-label": textContent.titleLabel || "Titre"
})}
/>
<select
className="task-planner-type-select"
className="task-planner-type-select nds-select"
data-size="xs"
value={task.type}
onChange={(event) => onUpdateTask(task.id, (current) => {
const nextType = event.target.value;
@ -104,7 +110,8 @@ export function TaskPlannerItem({
)}
<div className="task-planner-actions">
<button
className={`task-planner-dropdown-button ${descriptionOpen ? "active" : ""}`}
className={`task-planner-dropdown-button nds-icon-button ${descriptionOpen ? "active" : ""}`}
data-size="xs"
type="button"
onClick={onToggleDescription}
aria-expanded={descriptionOpen}
@ -114,7 +121,8 @@ export function TaskPlannerItem({
<Icon name={descriptionOpen ? "chevron-up" : "chevron-down"} />
</button>
<button
className={`task-planner-settings-button ${settingsOpen ? "active" : ""}`}
className={`task-planner-settings-button nds-icon-button ${settingsOpen ? "active" : ""}`}
data-size="xs"
type="button"
onClick={onToggleSettings}
aria-expanded={settingsOpen}
@ -123,7 +131,7 @@ export function TaskPlannerItem({
>
<Icon name="settings" />
</button>
<button className="task-planner-delete-button danger" type="button" onClick={() => onDeleteTask(task.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${task.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDeleteTask(task.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${task.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -110,7 +110,9 @@ function TaskDescriptionPanel({ task, tasks, parentMap, parentId, textContent, o
<span>{textContent.categoryLabel || "Catégorie"}</span>
<input
{...categoryEdit.getInputProps({
className: "nds-inline-input",
disabled: !categoryEditable,
type: "text",
placeholder: textContent.categoryPlaceholder || "Nom de catégorie",
"aria-label": `${textContent.categoryLabel || "Catégorie"} ${task.title}`
})}
@ -120,7 +122,8 @@ function TaskDescriptionPanel({ task, tasks, parentMap, parentId, textContent, o
<span>{textContent.descriptionLabel || "Description"}</span>
<textarea
{...descriptionEdit.getInputProps({
className: "task-planner-description",
className: "task-planner-description nds-textarea",
"data-size": "sm",
placeholder: textContent.descriptionPlaceholder || "Description",
"aria-label": `${textContent.descriptionLabel || "Description"} ${task.title}`
})}
@ -138,6 +141,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
<span>{textContent.taskDailyResetTimeLabel || "Heure quotidienne"}</span>
<div className="task-planner-reset-control">
<input
className="nds-time"
data-size="xs"
type="time"
value={task.dailyResetTime || data.resetTime}
onChange={(event) => onUpdateTask(task.id, (current) => {
@ -149,6 +154,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
aria-label={`${textContent.taskDailyResetTimeLabel || "Heure quotidienne"} ${task.title}`}
/>
<button
className="task-planner-reset-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onUpdateTask(task.id, (current) => {
const nextTask = { ...current };
@ -171,6 +178,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
<label className="task-planner-reset-override">
<span>{textContent.taskWeeklyResetDayLabel || "Jour hebdo"}</span>
<select
className="nds-select"
data-size="xs"
value={task.weeklyResetDay ?? ""}
onChange={(event) => onUpdateTask(task.id, (current) => {
const nextTask = { ...current };
@ -212,6 +221,8 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
<div className="task-planner-relations">
<span>{label}</span>
<select
className="nds-select"
data-size="sm"
value=""
onChange={(event) => {
onAddLinkTarget(task.id, event.target.value);
@ -228,6 +239,8 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
<span>{getTaskTitle(tasks, relation.toTaskId)}</span>
<label title={prerequisiteLabel}>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={relation.prerequisite}
onChange={(event) => onSetPrerequisite(task.id, relation.toTaskId, event.target.checked)}
@ -235,7 +248,7 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
/>
<span>{prerequisiteLabel}</span>
</label>
<button type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
<Icon name="close" />
</button>
</div>

View file

@ -7,17 +7,19 @@ export function TaskPlannerSettings({ data, textContent, onWeeklyResetDayChange,
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
<label>
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
<select value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
<select className="nds-select" data-size="sm" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
<input className="nds-time" data-size="sm" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
</label>
<div className="task-planner-settings-actions">
<button
className="task-planner-clear-completed-button danger"
className="task-planner-clear-completed-button nds-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={onClearCompletedUniqueTasks}
disabled={!data.tasks.some((task) => task.type === "unique" && task.checked)}

View file

@ -23,7 +23,8 @@ export function TaskPlannerTopbar({
<div className="task-planner-topbar">
<div className="task-planner-global-controls">
<button
className={`tool-split-scroll-toggle task-planner-hide-completed-toggle ${data.hideCompleted ? "active" : ""}`}
className={`tool-split-scroll-toggle task-planner-hide-completed-toggle nds-switch ${data.hideCompleted ? "active" : ""}`}
data-size="lg"
type="button"
onClick={onHideCompletedToggle}
aria-pressed={data.hideCompleted}
@ -34,7 +35,8 @@ export function TaskPlannerTopbar({
<i aria-hidden="true" />
</button>
<button
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
className={`task-planner-settings-toggle nds-button ${settingsOpen ? "active" : ""}`}
data-size="sm"
type="button"
onClick={onSettingsToggle}
aria-expanded={settingsOpen}
@ -48,11 +50,11 @@ export function TaskPlannerTopbar({
{editing && (
<div className="task-planner-add-stack">
<form className="inline-form task-planner-add-form" onSubmit={onAddTask}>
<input {...taskDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
<select {...taskDraft.getFieldProps("type", { "aria-label": textContent.typeLabel || "Type" })}>
<input {...taskDraft.getFieldProps("title", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
<select {...taskDraft.getFieldProps("type", { className: "nds-select", "data-size": "sm", "aria-label": textContent.typeLabel || "Type" })}>
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
</select>
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}

View file

@ -9,11 +9,11 @@ export function TextExchangeActions({ type, data, context, onImport, textContent
return (
<>
<div className="text-import-actions">
<button type="button" onClick={exchange.openImport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport}>
<Icon name="import" />
<span>{textContent.importOpenButton || "Importer du texte"}</span>
</button>
<button type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
<Icon name="export" />
<span>{textContent.exportOpenButton || exportLabel || "Exporter en format texte"}</span>
</button>

View file

@ -24,14 +24,15 @@ export function TextImportModal({ title, value, placeholder, submitLabel, readOn
<section className="confirm-modal text-import-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="text-import-title">
<header>
<h2 id="text-import-title">{title}</h2>
<button className="drawer-close-button" type="button" onClick={onClose} aria-label="Fermer" title="Fermer">
<button className="drawer-close-button nds-icon-button" data-size="sm" type="button" onClick={onClose} aria-label="Fermer" title="Fermer">
<Icon name="close" />
</button>
</header>
<form className="text-import-modal-form" onSubmit={onSubmit}>
<textarea
autoFocus
className={error ? "has-error" : ""}
className={`nds-textarea ${error ? "has-error" : ""}`.trim()}
data-size="sm"
readOnly={readOnly}
value={value}
onChange={(event) => onChange?.(event.target.value)}
@ -53,11 +54,11 @@ export function TextImportModal({ title, value, placeholder, submitLabel, readOn
</p>
)}
<footer>
<button type="button" onClick={onClose}>Annuler</button>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>Annuler</button>
{readOnly ? (
<button className="primary" type="button" onClick={onCopy}>{submitLabel}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={onCopy}>{submitLabel}</button>
) : (
<button className="primary" type="submit">{submitLabel}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{submitLabel}</button>
)}
</footer>
</form>

View file

@ -18,11 +18,11 @@ export function CountdownControls({ textContent, form, error, onChange, onSubmit
<form className="timer-control-panel timer-countdown-form" role="tabpanel" onSubmit={onSubmit} aria-describedby={error ? "timer-countdown-error" : undefined}>
<label>
<span>{textContent.countdownLabel || "Libellé"}</span>
<input value={form.label} onChange={(event) => onChange({ label: event.target.value })} placeholder={textContent.countdownLabelPlaceholder || "Boss, event, craft..."} />
<input className="nds-input" data-size="sm" type="text" value={form.label} onChange={(event) => onChange({ label: event.target.value })} placeholder={textContent.countdownLabelPlaceholder || "Boss, event, craft..."} />
</label>
<label>
<span>{textContent.countdownTypeLabel || "Type"}</span>
<select value={form.type} onChange={(event) => onChange({ type: event.target.value })}>
<select className="nds-select" data-size="sm" value={form.type} onChange={(event) => onChange({ type: event.target.value })}>
<option value="duration">{textContent.durationType || "Durée"}</option>
<option value="daily_time">{textContent.dailyTimeType || "Heure précise"}</option>
<option value="time_pattern">{textContent.timePatternType || "Pattern horaire"}</option>
@ -68,7 +68,7 @@ export function CountdownControls({ textContent, form, error, onChange, onSubmit
<span>{error}</span>
</p>
)}
<button className="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
</form>
);
}
@ -102,7 +102,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${(canReset || canToggleRefresh) && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
{reorderEnabled && (
<button
className="timer-drag-handle"
className="timer-drag-handle nds-icon-button"
data-size="xs"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, countdown.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${countdown.label}`}
@ -135,7 +136,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
<span className="timer-inline-controls">
{canReset && (
<button
className="timer-inline-reset"
className="timer-inline-reset nds-icon-button"
data-size="xxs"
type="button"
onClick={(event) => {
event.stopPropagation();
@ -149,7 +151,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
)}
{canToggleRefresh && (
<button
className={`timer-inline-refresh-toggle ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
className={`timer-inline-refresh-toggle nds-switch ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
data-size="xs"
type="button"
onClick={(event) => {
event.stopPropagation();
@ -174,7 +177,7 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
</span>
</span>
)}
<button className="danger" type="button" onClick={() => onDelete(countdown.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(countdown.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -286,12 +286,13 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
</section>
<section className={`tool-split-results timer-results-card ${data.scrollResults ? "is-scrollable" : ""}`}>
<div className="tool-split-actions">
<button className="tool-split-action-button danger" type="button" onClick={resetActiveResults} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={resetActiveResults} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
{activeTab === "countdown" && (
<button
className={`tool-split-scroll-toggle timer-sort-toggle ${data.sortResults ? "active" : ""}`}
className={`tool-split-scroll-toggle timer-sort-toggle nds-switch ${data.sortResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setSortResults(!data.sortResults)}
aria-pressed={data.sortResults}
@ -303,7 +304,8 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
</button>
)}
<button
className={`tool-split-scroll-toggle timer-scroll-toggle ${data.scrollResults ? "active" : ""}`}
className={`tool-split-scroll-toggle timer-scroll-toggle nds-switch ${data.scrollResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollResults(!data.scrollResults)}
aria-pressed={data.scrollResults}

View file

@ -26,7 +26,8 @@ export function TimerAlertModeSwitch({ mode, blocked, textContent, onChange }) {
return (
<div
className={`timer-alert-mode-switch ${blocked ? "is-blocked" : ""}`}
className={`timer-alert-mode-switch nds-radio-button-group ${blocked ? "is-blocked" : ""}`}
data-size="xs"
role="group"
aria-label={textContent.alertModeTitle || "Mode d'alerte"}
title={blocked ? blockedTitle : undefined}
@ -63,11 +64,11 @@ export function TimePartsInput({ value, onChange, allowWildcard = false }) {
return (
<div className="time-parts-input" role="group" aria-label="Format heure minute seconde">
<input value={value.hours} onChange={(event) => updatePart("hours", event.target.value)} placeholder="hh" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.hours} onChange={(event) => updatePart("hours", event.target.value)} placeholder="hh" inputMode={allowWildcard ? "text" : "numeric"} />
<span>:</span>
<input value={value.minutes} onChange={(event) => updatePart("minutes", event.target.value)} placeholder="mm" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.minutes} onChange={(event) => updatePart("minutes", event.target.value)} placeholder="mm" inputMode={allowWildcard ? "text" : "numeric"} />
<span>:</span>
<input value={value.seconds} onChange={(event) => updatePart("seconds", event.target.value)} placeholder="ss" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.seconds} onChange={(event) => updatePart("seconds", event.target.value)} placeholder="ss" inputMode={allowWildcard ? "text" : "numeric"} />
</div>
);
}
@ -87,6 +88,7 @@ export function EditableTimerLabel({ value, placeholder, ariaLabel, children, on
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label timer-lap-label",
type: "text",
placeholder,
"aria-label": ariaLabel
})}

View file

@ -10,13 +10,13 @@ export function StopwatchControls({ textContent, elapsedMs, running, onStart, on
<span>{textContent.stopwatchTitle || "Chronomètre à étapes"}</span>
<strong><TimerValue ms={elapsedMs} compact /></strong>
<div className="timer-actions">
<button className="primary timer-action-button" type="button" onClick={running ? onPause : onStart} aria-label={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"} title={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"}>
<button className="timer-action-button nds-icon-button" data-size="sm" data-variant="primary" type="button" onClick={running ? onPause : onStart} aria-label={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"} title={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"}>
<Icon name={running ? "pause-circle" : "play-circle"} />
</button>
<button className="timer-action-button" type="button" onClick={onLap} disabled={!elapsedMs} aria-label={textContent.lapButton || "Étape"} title={textContent.lapButton || "Étape"}>
<button className="timer-action-button nds-icon-button" data-size="sm" type="button" onClick={onLap} disabled={!elapsedMs} aria-label={textContent.lapButton || "Étape"} title={textContent.lapButton || "Étape"}>
<Icon name="record-circle" />
</button>
<button className="danger timer-action-button" type="button" onClick={onReset} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="timer-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={onReset} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="stop-circle" />
</button>
</div>
@ -56,7 +56,7 @@ export function StopwatchLapList({ laps, textContent, onRename, onDelete }) {
</span>
</span>
)}
<button className="danger" type="button" onClick={() => onDelete(lap.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(lap.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -188,7 +188,9 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<header>
<div>
<button
className="module-drag-handle"
className="module-drag-handle nds-icon-button"
data-size="md"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, module.id)}
aria-label={`Déplacer ${module.title || label}`}
@ -204,7 +206,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<div>
{definition.scrollable && (
<button
className={`module-scroll-button ${scrollable ? "active" : ""}`}
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
data-size="lg"
onClick={() => onUpdateModule(module.id, (currentModule) => {
const nextModule = { ...currentModule };
if (scrollable) {
@ -224,7 +227,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
)}
{hasAddPanel && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
data-size="md"
onClick={() => setEditing((value) => {
const nextEditing = !value;
if (nextEditing) setTextExchangeOpen(false);
@ -239,7 +243,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
)}
{hasTextExchange && (
<button
className={`module-exchange-button ${textExchangeOpen ? "active" : ""}`}
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
data-size="md"
onClick={() => setTextExchangeOpen((value) => {
const nextOpen = !value;
if (nextOpen) setEditing(false);
@ -252,7 +257,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<Icon name="import" />
</button>
)}
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
<button className="module-delete-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
</button>
</div>

View file

@ -6,7 +6,7 @@ export const HOUR_HALF_SLOT_COUNT = 2;
export const WEEK_SLOT_COUNT = DAY_HOUR_COUNT * HOUR_HALF_SLOT_COUNT;
export const WEEK_HOUR_ROW_HEIGHT = 27;
export const WEEK_HIDDEN_ROW_HEIGHT = 20;
export const WEEK_COMPACT_EVENT_HEIGHT = WEEK_HOUR_ROW_HEIGHT / HOUR_HALF_SLOT_COUNT;
export const WEEK_COMPACT_EVENT_HEIGHT = 22;
export const WEEKDAY_LABELS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
function occurrencesOverlap(first, second) {

View file

@ -6,6 +6,15 @@ export const CALENDAR_MONTH_REPEAT_MODES = ["none", "weekly", "monthly"];
export const CALENDAR_ALERT_MODES = ["off", "visible", "site"];
export const CALENDAR_ALERT_OFFSETS = [10, 5, 0];
export const CALENDAR_COLORS = ["violet", "gold", "cyan", "pink", "green", "gray"];
export const CALENDAR_COLOR_VALUES = {
violet: "#8b5cf6",
gold: "#f6c453",
cyan: "#22d3ee",
pink: "#d946ef",
green: "#22c55e",
gray: "#7d879f"
};
export const CALENDAR_DARK_TEXT_COLORS = new Set(["gold", "cyan", "green"]);
export const WEEK_SLOT_MINUTES = 30;
export const WEEK_TIME_INPUT_STEP_MINUTES = 5;
export const WEEK_SLOT_COUNT = 48;
@ -13,6 +22,14 @@ export const DAY_MS = 24 * 60 * 60 * 1000;
export const MINUTE_MS = 60 * 1000;
export const WEEK_DAY_COUNT = 7;
export function getCalendarColorValue(color) {
return CALENDAR_COLOR_VALUES[color] || CALENDAR_COLOR_VALUES.violet;
}
export function getCalendarEventSurfaceVariant(color) {
return CALENDAR_DARK_TEXT_COLORS.has(color) ? "dark" : "default";
}
export function pad2(value) {
return String(value).padStart(2, "0");
}

View file

@ -112,7 +112,7 @@ export function AboutPage({ siteContent }) {
</article>
))}
</div>
<a className="button primary about-tools-library-button" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
<a className="about-tools-library-button nds-button" data-size="md" data-variant="primary" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
</section>
{content.contribute && (
<section className="about-contribute-section nebula-panel" id="contribuer" aria-labelledby="about-contribute-title">

View file

@ -0,0 +1,641 @@
// 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";
import { ToastPositionSwitch } from "../components/ToastPositionSwitch.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"],
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 NAV_ITEMS = [
{ id: "foundations", label: "Foundations" },
...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");
const [toastPosition, setToastPosition] = useState("right");
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", "Toast switch"]}
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>,
<ControlSample label="ToastPositionSwitch"><ToastPositionSwitch position={toastPosition} labels={{ left: "Notifications à gauche", right: "Notifications à droite", title: "Position des notifications" }} onChange={setToastPosition} /></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=&quot;nds-button&quot;</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} />)}
<DatePickerButtonsSection />
<SpecializedComponentsSection />
<AuditSection />
</main>
</div>
);
}

View file

@ -47,7 +47,7 @@ export function HomeOnboardingSection({ content, className = "", onCreateToolbox
</ol>
{onCreateToolbox && content.primaryAction && (
<div className="home-onboarding-steps-action">
<button className="primary home-onboarding-create-button" type="button" onClick={onCreateToolbox}>
<button className="home-onboarding-create-button nds-button" data-size="md" data-variant="primary" type="button" onClick={onCreateToolbox}>
{content.primaryAction}
</button>
</div>

View file

@ -15,8 +15,8 @@ export function HomePage({ siteContent, toolboxes, actions }) {
<h1>{content.hero.title}</h1>
<p>{content.hero.description}</p>
<div className="actions">
<a className="button primary" href="#/toolboxes">{content.hero.primaryAction}</a>
<a className="button" href="#/games">{content.hero.secondaryAction}</a>
<a className="nds-button" data-size="md" data-variant="primary" href="#/toolboxes">{content.hero.primaryAction}</a>
<a className="nds-button" data-size="md" href="#/games">{content.hero.secondaryAction}</a>
</div>
</div>
<div className="home-stats hero-stats" aria-label="Statistiques locales du site">

View file

@ -356,7 +356,8 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
<div className="library-example-actions">
{definition.scrollable && (
<button
className={`module-scroll-button ${scrollable ? "active" : ""}`}
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollable((value) => !value)}
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${title}`}
@ -369,7 +370,8 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
)}
{definition.editable && definition.addable !== false && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
data-size="sm"
type="button"
onClick={() => setEditing((value) => !value)}
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${title}`}
@ -379,7 +381,7 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
<Icon name="add" />
</button>
)}
<button className="library-reset-button" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
<button className="library-reset-button nds-icon-button" data-size="sm" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
<Icon name="refresh" />
</button>
</div>
@ -506,7 +508,12 @@ function ControlPreview({ item }) {
return (
<span className="library-control-preview" aria-hidden="true">
{buttons.map((button, index) => (
<span className={`notepad-toolbar-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""} ${button.danger ? "danger" : ""}`} key={`${button.label || button.icon || index}-${index}`}>
<span
className={`notepad-toolbar-button nds-icon-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""}`.trim()}
data-size="sm"
data-variant={button.danger ? "danger" : undefined}
key={`${button.label || button.icon || index}-${index}`}
>
{button.icon ? <Icon name={button.icon} /> : button.label}
</span>
))}
@ -516,7 +523,7 @@ function ControlPreview({ item }) {
return (
<span className="library-control-preview" aria-hidden="true">
<span className={`notepad-toolbar-button ${preview.active ? "active" : ""} ${preview.danger ? "danger" : ""}`}>
<span className={`notepad-toolbar-button nds-icon-button ${preview.active ? "active" : ""}`.trim()} data-size="sm" data-variant={preview.danger ? "danger" : undefined}>
<Icon name={preview.icon || item.icon || "settings"} />
</span>
</span>
@ -582,7 +589,7 @@ function ToolAdvancedFeatureList({ docs, heading, copyLabel, copySuccess, contex
<p>{docs.importFormat.text}</p>
<div className="library-tool-import-example">
<pre><code>{docs.importFormat.example}</code></pre>
<button type="button" onClick={copyExample} title={copyLabel || "Copier l'exemple"}>
<button className="nds-button" data-size="sm" type="button" onClick={copyExample} title={copyLabel || "Copier l'exemple"}>
<Icon name="copy" />
<span>{copied ? copySuccess || "Exemple copié." : copyLabel || "Copier l'exemple"}</span>
</button>

View file

@ -9,6 +9,9 @@ const GamesPage = lazy(() => import("../features/games/GamesPage.jsx").then((mod
const LibraryPage = lazy(() => import("../pages/LibraryPage.jsx").then((module) => ({ default: module.LibraryPage })));
const ToolboxPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxPage })));
const ToolboxesPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxesPage })));
const DesignSystemPage = import.meta.env.DEV
? lazy(() => import("../pages/DesignSystemPage.jsx").then((module) => ({ default: module.DesignSystemPage })))
: null;
function RouteLoader() {
return <section className="empty"><p>Chargement...</p></section>;
@ -19,6 +22,7 @@ export function RouteContent(props) {
if (route === "/") return <HomePage {...props} />;
if (route === "/about") return <AboutPage siteContent={props.siteContent} />;
if (route === "/library") return <Suspense fallback={<RouteLoader />}><LibraryPage {...props} /></Suspense>;
if (route === "/design-system" && DesignSystemPage) return <Suspense fallback={<RouteLoader />}><DesignSystemPage /></Suspense>;
if (route === "/toolboxes") return <Suspense fallback={<RouteLoader />}><ToolboxesPage {...props} /></Suspense>;
if (route.startsWith("/toolbox/")) return <Suspense fallback={<RouteLoader />}><ToolboxPage {...props} toolboxId={route.split("/")[2]} /></Suspense>;
if (route === "/games") return <Suspense fallback={<RouteLoader />}><GamesPage {...props} /></Suspense>;

View file

@ -1,4 +1,6 @@
// Rôle : définit les bases globales HTML, body, typographie et contrôles natifs.
@use "design-system/controls";
* {
box-sizing: border-box;
}
@ -36,52 +38,73 @@ a {
text-decoration: none;
}
button,
.button,
select,
input,
textarea,
.import-button {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.92);
color: var(--color-text-primary);
font: inherit;
transition:
transform var(--duration-fast) var(--ease-standard),
border-color var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
button {
@include controls.text-button;
}
button,
.button,
.import-button {
display: inline-flex;
min-height: 42px;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
cursor: pointer;
select {
@include controls.select-control;
}
button:hover,
.button:hover,
.import-button:hover {
border-color: rgba(196, 181, 253, 0.34);
background:
linear-gradient(135deg, rgba(31, 37, 68, 0.96), rgba(22, 26, 51, 0.96)) padding-box,
linear-gradient(135deg, rgba(139, 92, 246, 0.32), rgba(196, 181, 253, 0.2)) border-box;
box-shadow:
0 0 14px rgba(139, 92, 246, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
select:hover,
select:focus {
@include controls.select-control-hover;
}
button:active,
.button:active,
.import-button:active {
select:disabled {
@include controls.control-disabled;
}
select option {
@include controls.select-option;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea) {
@include controls.field-control;
}
input[type="time"] {
@include controls.time-control;
}
:where(textarea) {
@include controls.field-textarea;
@include controls.scrollbar-control;
@include controls.scrollbar-resizer;
}
:where(input, textarea)::placeholder {
@include controls.field-placeholder;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):hover,
input[type="time"]:hover {
@include controls.field-hover;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):focus,
input[type="time"]:focus {
@include controls.field-focus;
outline: none;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):focus-visible,
input[type="time"]:focus-visible {
outline: 2px solid var(--color-accent-gold);
outline-offset: 3px;
}
button:disabled,
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):disabled,
input[type="time"]:disabled {
@include controls.control-disabled;
}
button:hover:not(:disabled, .active, .is-active, [aria-pressed="true"], .nds-button, .nds-icon-button) {
@include controls.text-button-hover;
}
button:active {
transform: translateY(0);
}
@ -91,66 +114,31 @@ button:active,
}
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
display: inline-grid;
width: 20px;
min-width: 20px;
max-width: 20px;
inline-size: 20px;
min-inline-size: 20px;
max-inline-size: 20px;
aspect-ratio: 1;
height: 20px;
min-height: 20px;
max-height: 20px;
block-size: 20px;
min-block-size: 20px;
max-block-size: 20px;
place-content: center;
margin: 0;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
line-height: 0;
background:
linear-gradient(135deg, rgba(27, 34, 66, 0.92), rgba(16, 20, 44, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.5), rgba(139, 92, 246, 0.44)) border-box;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 0 0 1px rgba(5, 7, 17, 0.42);
cursor: pointer;
@include controls.checkbox-control;
}
input[type="checkbox"]::before {
content: "";
width: 11px;
height: 11px;
background: #fff;
transform: scale(0);
transition: transform var(--duration-fast) var(--ease-standard);
clip-path: polygon(14% 52%, 0 66%, 38% 100%, 100% 18%, 84% 5%, 34% 68%);
@include controls.checkbox-check;
}
input[type="checkbox"]:hover {
border-color: rgba(246, 196, 83, 0.6);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
0 0 14px rgba(246, 196, 83, 0.14);
@include controls.checkbox-hover;
}
input[type="checkbox"]:checked {
border-color: rgba(246, 196, 83, 0.88);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(139, 92, 246, 0.86)) padding-box,
linear-gradient(135deg, var(--color-accent-gold), rgba(196, 181, 253, 0.88)) border-box;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
0 0 16px rgba(246, 196, 83, 0.2);
@include controls.checkbox-checked;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
@include controls.checkbox-checked-check;
}
input[type="checkbox"]:disabled {
@include controls.checkbox-disabled;
}
input[type="checkbox"]:disabled::before {
opacity: 0.54;
}
input[type="checkbox"]:focus-visible {
@ -160,38 +148,24 @@ input[type="checkbox"]:focus-visible {
.themed-scrollbar,
.legacy-scrollbar {
scrollbar-color: rgb(79, 26, 114) transparent;
scrollbar-width: thin;
@include controls.scrollbar-control;
}
.filter-options::-webkit-scrollbar,
.drawer-panel::-webkit-scrollbar,
.damage-table-wrap::-webkit-scrollbar,
.themed-scrollbar::-webkit-scrollbar,
.legacy-scrollbar::-webkit-scrollbar {
background-color: transparent;
width: 6px;
height: 6px;
border-radius: 0;
}
.filter-options::-webkit-scrollbar-track,
.drawer-panel::-webkit-scrollbar-track,
.damage-table-wrap::-webkit-scrollbar-track,
.themed-scrollbar::-webkit-scrollbar-track,
.legacy-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 0;
box-shadow: none;
}
.filter-options::-webkit-scrollbar-thumb,
.drawer-panel::-webkit-scrollbar-thumb,
.damage-table-wrap::-webkit-scrollbar-thumb,
.themed-scrollbar::-webkit-scrollbar-thumb,
.legacy-scrollbar::-webkit-scrollbar-thumb {
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.5, rgb(79, 26, 114)), color-stop(1, rgb(44, 1, 135)));
border-radius: 10px;
:is(
.annotation-marker-list,
.damage-table-wrap,
.drawer-panel,
.filter-options,
.goal-tree-editor-panel,
.goal-tree-prerequisite-list,
.goal-tree-view-wrap,
.module-content,
.notepad-pages-list,
.notepad-surface,
.table-grid-wrap,
.tool-split-tree
) {
@include controls.scrollbar-control;
}
body.is-modal-open {
@ -203,238 +177,3 @@ body.is-modal-open {
align-items: center;
justify-content: center;
}
.primary {
border-color: rgba(246, 196, 83, 0.34);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
box-shadow:
0 8px 22px rgba(5, 7, 17, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
color: #fff;
font-weight: 700;
}
.primary:hover {
border-color: rgba(246, 196, 83, 0.72);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(196, 181, 253, 0.68)) border-box;
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.16),
0 0 18px rgba(139, 92, 246, 0.3),
0 0 30px rgba(246, 196, 83, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.ghost {
border-color: transparent;
background: transparent;
color: var(--color-text-secondary);
}
.ghost:hover {
border-color: rgba(196, 181, 253, 0.18);
background: rgba(139, 92, 246, 0.08);
color: var(--color-text-primary);
box-shadow: none;
}
.danger {
color: var(--color-danger);
}
.danger:hover {
border-color: rgba(251, 113, 133, 0.48);
background:
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
linear-gradient(135deg, rgba(251, 113, 133, 0.56), rgba(196, 181, 253, 0.16)) border-box;
color: #fff;
box-shadow:
0 0 14px rgba(251, 113, 133, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) {
display: inline-flex;
width: 40px;
min-width: 40px;
min-height: 40px;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.88);
color: var(--color-text-primary);
font: inherit;
cursor: pointer;
transition:
transform var(--duration-fast) var(--ease-standard),
border-color var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) .ui-icon {
transition:
background-color var(--duration-fast) var(--ease-standard),
filter var(--duration-fast) var(--ease-standard);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover {
border-color: rgba(196, 181, 253, 0.32);
background:
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
color: #ddd6fe;
box-shadow:
0 0 12px rgba(139, 92, 246, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.tool-quick-add-button
).primary {
border-color: rgba(246, 196, 83, 0.34);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
color: #fff;
box-shadow:
0 8px 22px rgba(5, 7, 17, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.tool-quick-add-button
).primary:hover {
border-color: rgba(246, 196, 83, 0.72);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(196, 181, 253, 0.68)) border-box;
color: #fff;
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.16),
0 0 18px rgba(139, 92, 246, 0.3),
0 0 30px rgba(246, 196, 83, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover .ui-icon {
background-color: #c4b5fd;
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger {
border-color: rgba(251, 113, 133, 0.2);
color: var(--color-danger);
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover {
border-color: rgba(251, 113, 133, 0.56);
background:
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
linear-gradient(135deg, rgba(251, 113, 133, 0.56), rgba(196, 181, 253, 0.16)) border-box;
color: #fff;
box-shadow:
0 0 14px rgba(251, 113, 133, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover .ui-icon {
background-color: #fff;
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
}

View file

@ -452,5 +452,5 @@
}
.card-icon-button {
flex: 0 0 40px;
flex: 0 0 auto;
}

View file

@ -0,0 +1,514 @@
// Rôle : met en page le catalogue de contrôles visible uniquement en développement.
@use "design-system/controls";
@use "design-system/interactive-controls";
.design-system-page {
display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
gap: var(--space-6);
width: min(1480px, calc(100% - 32px));
margin: 0 auto;
padding: var(--space-6) 0;
}
.design-system-nav {
position: sticky;
top: var(--space-4);
display: grid;
max-height: calc(100vh - var(--space-6));
align-content: start;
gap: var(--space-4);
padding: var(--space-5);
overflow: auto;
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background:
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 34%),
rgba(7, 10, 24, 0.66);
box-shadow: var(--shadow-md);
}
.design-system-nav h1,
.design-system-section h2,
.design-system-matrix h3,
.design-system-nav p {
margin: 0;
}
.design-system-nav h1 {
font-size: clamp(2rem, 4vw, 3.2rem);
line-height: 0.95;
}
.design-system-nav p:not(.eyebrow) {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.design-system-nav nav,
.design-system-token-stack {
display: grid;
gap: 8px;
}
.design-system-nav nav {
padding-top: var(--space-2);
border-top: 1px solid rgba(150, 165, 205, 0.12);
}
.design-system-nav button {
display: flex;
min-height: 34px;
align-items: center;
padding: 0 10px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
text-align: left;
cursor: pointer;
}
.design-system-nav button:hover,
.design-system-nav button:focus-visible {
border-color: rgba(246, 196, 83, 0.28);
background: rgba(246, 196, 83, 0.08);
color: var(--color-text-primary);
}
.design-system-main {
display: grid;
min-width: 0;
gap: var(--space-6);
}
.design-system-section {
display: grid;
min-width: 0;
scroll-margin-top: var(--space-5);
gap: var(--space-4);
}
.design-system-section > header {
display: flex;
min-width: 0;
align-items: end;
justify-content: space-between;
gap: var(--space-4);
padding: 0;
border: 0;
}
.design-system-section h2 {
font-size: clamp(1.6rem, 3vw, 2.35rem);
line-height: 1;
}
.design-system-component-intro {
display: grid;
grid-template-columns: minmax(180px, 0.8fr) minmax(0, 2.4fr);
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.34);
}
.design-system-doc-summary,
.design-system-attributes-doc {
display: grid;
align-content: start;
gap: 8px;
min-width: 0;
}
.design-system-component-intro span:first-child {
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-component-intro strong {
color: var(--color-text-primary);
font-size: var(--font-size-lg);
}
.design-system-component-intro code,
.design-system-component-intro li code {
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
.design-system-attributes-doc dl {
display: grid;
overflow: hidden;
margin: 0;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.22);
}
.design-system-attributes-doc dl > div {
display: grid;
grid-template-columns: minmax(118px, 0.42fr) minmax(0, 1fr);
min-width: 0;
border-top: 1px solid rgba(165, 180, 252, 0.1);
}
.design-system-attributes-doc dl > div:first-child {
border-top: 0;
}
.design-system-attributes-doc dt,
.design-system-attributes-doc dd {
min-width: 0;
margin: 0;
padding: 8px 10px;
}
.design-system-attributes-doc dt {
border-right: 1px solid rgba(165, 180, 252, 0.1);
background: rgba(139, 92, 246, 0.06);
}
.design-system-attributes-doc dd {
display: flex;
flex-wrap: wrap;
gap: 6px;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 800;
}
.design-system-attributes-doc dd code {
padding: 2px 6px;
border: 1px solid rgba(246, 196, 83, 0.16);
border-radius: var(--radius-sm);
background: rgba(246, 196, 83, 0.06);
}
.design-system-matrix {
display: grid;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.42);
box-shadow: var(--shadow-sm);
}
.design-system-matrix > header {
display: flex;
min-width: 0;
align-items: start;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-3);
border-bottom: 1px solid rgba(150, 165, 205, 0.12);
}
.design-system-matrix h3 {
font-size: var(--font-size-lg);
}
.design-system-matrix p {
margin: 4px 0 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.design-system-config-panel {
display: flex;
width: 100%;
min-width: 0;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.design-system-config-field {
display: grid;
min-width: 118px;
gap: 4px;
}
.design-system-config-field span {
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 900;
text-transform: uppercase;
}
.design-system-matrix-grid {
@include controls.scrollbar-control;
display: grid;
grid-template-columns: 136px repeat(var(--nds-matrix-columns), minmax(150px, 1fr));
min-width: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
}
.design-system-matrix-grid > * {
min-width: 0;
border-top: 1px dashed rgba(139, 92, 246, 0.42);
border-left: 1px dashed rgba(139, 92, 246, 0.42);
}
.design-system-matrix-corner,
.design-system-matrix-heading,
.design-system-matrix-row-label {
display: flex;
min-height: 48px;
align-items: center;
padding: 10px 12px;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-matrix-heading {
color: var(--color-accent-gold);
}
.design-system-matrix-row-label {
display: grid;
align-content: center;
gap: 3px;
background: rgba(5, 7, 17, 0.22);
line-height: 1.25;
overflow-wrap: anywhere;
}
.design-system-matrix-row-label span {
color: var(--color-text-muted);
font-size: 0.66rem;
}
.design-system-matrix-row-label strong {
color: var(--color-text-secondary);
font-size: 0.78rem;
}
.design-system-matrix-cell {
display: flex;
min-height: 104px;
align-items: center;
gap: 10px;
padding: 12px;
}
.design-system-matrix-separator {
grid-column: 1 / -1;
display: flex;
min-height: 38px;
align-items: center;
padding: 8px 12px;
border-top: 2px solid rgba(246, 196, 83, 0.34);
border-left: 0;
background: rgba(246, 196, 83, 0.055);
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-control-sample {
display: grid;
width: 100%;
min-width: 0;
min-height: 48px;
align-items: center;
justify-items: center;
}
.design-system-sample-preview {
display: flex;
width: 100%;
min-width: 0;
align-items: center;
justify-content: center;
gap: 10px;
}
.design-system-matrix-cell > input,
.design-system-matrix-cell > select,
.design-system-matrix-cell > textarea,
.design-system-matrix-cell > .nds-field,
.design-system-matrix-cell > .mix-tags-input,
.design-system-control-sample input,
.design-system-control-sample select,
.design-system-control-sample textarea,
.design-system-control-sample .mix-tags-input {
width: 100%;
}
.design-system-foundations {
display: grid;
grid-template-columns: minmax(210px, 1.2fr) repeat(3, minmax(180px, 1fr));
gap: var(--space-3);
}
.design-system-foundations article {
display: grid;
min-height: 180px;
align-content: space-between;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.42);
box-shadow: var(--shadow-sm);
}
.design-system-foundations p {
margin: 0;
color: var(--color-text-primary);
font-size: var(--font-size-lg);
font-weight: 900;
}
.design-system-type-card strong {
display: block;
color: var(--color-text-primary);
font-size: clamp(4rem, 10vw, 7rem);
line-height: 0.9;
}
.design-system-type-card span {
color: var(--color-text-secondary);
}
.design-system-swatches {
display: grid;
grid-template-columns: repeat(6, minmax(24px, 1fr));
overflow: hidden;
border-radius: var(--radius-md);
}
.design-system-swatches span {
min-height: 96px;
background: var(--nds-swatch-color);
}
.design-system-radius-card > div,
.design-system-focus-card {
align-items: end;
}
.design-system-radius-card > div {
display: flex;
gap: 8px;
}
.design-system-radius-card span {
display: grid;
width: 52px;
height: 52px;
place-items: center;
border: 1px solid rgba(246, 196, 83, 0.32);
background: rgba(21, 26, 48, 0.72);
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
}
.design-system-spec-pill,
.design-system-size-spec {
display: inline-flex;
min-width: 0;
align-items: center;
}
.design-system-size-spec {
flex-wrap: wrap;
gap: 6px;
}
.design-system-spec-pill {
gap: 5px;
width: fit-content;
max-width: 100%;
padding: 3px 7px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-pill);
background: rgba(5, 7, 17, 0.28);
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 800;
}
.design-system-spec-pill b {
color: var(--color-accent-gold);
}
.design-system-inline-controls {
display: flex;
align-items: center;
gap: 6px;
}
.design-system-page .calendar-month-event-repeat-switch {
width: min(230px, 100%);
}
@media (max-width: 980px) {
.design-system-page {
grid-template-columns: 1fr;
}
.design-system-nav {
position: static;
max-height: none;
}
.design-system-nav nav {
grid-template-columns: repeat(auto-fit, minmax(138px, 1fr));
}
.design-system-foundations {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.design-system-component-intro {
grid-template-columns: 1fr;
}
.design-system-matrix > header {
display: grid;
}
.design-system-config-panel {
max-width: none;
justify-content: start;
}
}
@media (max-width: 640px) {
.design-system-page {
width: min(100% - 20px, 1480px);
padding-block: var(--space-4) calc(var(--space-4) + 88px);
}
.design-system-nav,
.design-system-foundations article,
.design-system-matrix > header {
padding: var(--space-3);
}
.design-system-foundations {
grid-template-columns: 1fr;
}
.design-system-matrix-grid {
grid-template-columns: 116px repeat(var(--nds-matrix-columns), minmax(140px, 1fr));
}
.design-system-matrix-cell {
padding: 12px;
}
}

View file

@ -62,4 +62,19 @@
box-shadow:
inset 0 -2px 0 rgba(246, 196, 83, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
&:hover:not(:disabled) {
border-color: rgba(246, 196, 83, 0.86);
background:
radial-gradient(circle at 100% 100%, rgba(246, 196, 83, 0.18), transparent 48%) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.2), rgba(139, 92, 246, 0.12) 52%, rgba(21, 26, 48, 0.08)) padding-box,
linear-gradient(135deg, rgba(16, 20, 38, 0.94), rgba(21, 26, 48, 0.86)) padding-box,
linear-gradient(120deg, rgba(246, 196, 83, 1), rgba(255, 244, 196, 0.92), rgba(246, 196, 83, 0.84)) border-box;
color: var(--color-text-primary);
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.18),
0 0 16px rgba(246, 196, 83, 0.18),
inset 0 -2px 0 rgba(246, 196, 83, 0.48),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
}

View file

@ -290,7 +290,6 @@
.sidebar-action-button {
display: inline-flex;
background: rgba(7, 10, 24, 0.36);
}
.content-shell {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
// Rôle : complète le catalogue dev avec les helpers de présentation non publics.
@use "controls";
.design-system-page {
.nds-control-row,
.nds-field {
display: grid;
gap: 6px;
min-width: 0;
}
.nds-field {
width: 100%;
}
.nds-label,
.nds-checkbox-label {
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-spec-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0;
}
.design-system-spec-list div {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 4px;
padding: 3px 6px;
border: 1px solid rgba(165, 180, 252, 0.1);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.28);
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 800;
}
.design-system-spec-list dt,
.design-system-spec-list dd {
margin: 0;
}
.design-system-spec-list dt {
color: var(--color-accent-gold);
}
.nds-checkbox-label {
display: inline-flex;
align-items: center;
gap: 8px;
text-transform: none;
}
.nds-date-picker-preview {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 3px;
width: min(300px, 100%);
padding: 4px;
border: 1px solid rgba(150, 165, 205, 0.16);
border-radius: var(--radius-sm);
background: rgba(8, 11, 24, 0.98);
}
.nds-date-picker-preview button {
@include controls.date-picker-button;
}
.nds-date-picker-preview button:hover,
.nds-date-picker-preview button.active {
@include controls.date-picker-button-hover;
}
.nds-date-picker-preview button.is-outside {
opacity: 0.32;
}
.nds-date-picker-preview-day {
display: grid;
min-height: 22px;
place-items: center;
color: var(--color-text-muted);
font-size: 0.66rem;
font-weight: 900;
text-transform: uppercase;
}
.nds-scrollbox-content-wide {
display: inline-flex;
width: max-content;
min-width: 1280px;
gap: 10px;
}
.nds-scrollbox-content-wide span {
display: inline-flex;
min-width: 170px;
min-height: 42px;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.58);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
}
}

View file

@ -76,15 +76,6 @@
font-size: var(--font-size-lg);
}
.filter-reset-button {
color: var(--color-text-secondary);
box-shadow: var(--shadow-sm);
}
.filter-reset-button:hover {
background: rgba(21, 26, 48, 0.96);
}
.field.compact {
padding: var(--space-4) 0;
}
@ -98,27 +89,14 @@
background: rgba(5, 7, 17, 0.36);
}
.filter-logic button {
min-height: 30px;
padding: 0 12px;
border-color: rgba(246, 196, 83, 0.24);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.72), rgba(45, 52, 146, 0.68)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.46), rgba(139, 92, 246, 0.28)) border-box;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-weight: 800;
text-transform: uppercase;
.filter-logic-group {
flex: 0 0 auto;
}
.filter-logic button:hover {
border-color: rgba(246, 196, 83, 0.52);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.86), rgba(67, 56, 202, 0.82)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.72), rgba(196, 181, 253, 0.42)) border-box;
box-shadow:
0 0 14px rgba(139, 92, 246, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
.filter-logic-group button {
min-height: 30px;
padding: 0 12px;
text-transform: uppercase;
}
.filter-options {
@ -151,22 +129,6 @@
color: var(--color-text-primary);
}
.filter-chip input {
flex: 0 0 18px;
width: 18px;
min-width: 18px;
max-width: 18px;
inline-size: 18px;
min-inline-size: 18px;
max-inline-size: 18px;
height: 18px;
min-height: 18px;
max-height: 18px;
block-size: 18px;
min-block-size: 18px;
max-block-size: 18px;
}
.filter-chip img {
width: 22px;
height: 22px;

View file

@ -20,26 +20,8 @@
}
.game-list-help-link {
display: inline-flex;
width: fit-content;
min-height: 36px;
align-items: center;
justify-content: center;
margin-top: var(--space-3);
border-color: rgba(165, 180, 252, 0.18);
background: rgba(5, 7, 17, 0.24);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
padding: 7px 12px;
}
.game-list-help-link:hover {
border-color: rgba(246, 196, 83, 0.44);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.42), rgba(196, 181, 253, 0.22)) border-box;
color: var(--color-text-primary);
}
.game-list-selector {
@ -244,13 +226,6 @@
width: 40px;
min-width: 40px;
min-height: 40px;
padding: 0;
border-radius: var(--radius-md);
}
.game-list-create-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.game-list-create-button .module-icon {

View file

@ -138,17 +138,6 @@ span {
gap: 10px;
}
.toolbar-stack {
align-items: flex-start;
flex-direction: column;
}
.toolbar-stack > div {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero-library-button {
display: inline-flex;
width: fit-content;
@ -198,4 +187,3 @@ span {
.home-section-heading p:not(.eyebrow) {
max-width: 980px;
}

Some files were not shown because too many files have changed in this diff Show more