@cyberskill/design 1.2.0 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +4 -3
- package/README.md +20 -19
- package/SKILL.md +5 -5
- package/THIRD-PARTY-NOTICES.md +35 -0
- package/VERSION +1 -1
- package/_ds_bundle.js +701 -1342
- package/_esm/cs.mjs +2 -2
- package/_esm/react.mjs +6 -1
- package/_vendor/NOTICE.md +16 -0
- package/_vendor/babel/README.md +7 -0
- package/_vendor/babel/babel.min.js +4 -0
- package/_vendor/react/README.md +3 -0
- package/_vendor/react/react-dom.production.min.js +1 -0
- package/_vendor/react/react.production.min.js +1 -0
- package/assets/aurora-hoa.webp +0 -0
- package/assets/aurora-kim.webp +0 -0
- package/assets/aurora-moc.webp +0 -0
- package/assets/aurora-tho.webp +0 -0
- package/assets/aurora-thuy.webp +0 -0
- package/base/ai.css +8 -3
- package/base/collections.css +10 -4
- package/base/components.css +5 -2
- package/base/controls.css +58 -21
- package/base/data.css +1 -1
- package/base/feedback.css +9 -2
- package/base/forms.css +1 -1
- package/base/glass.css +1 -1
- package/base/interaction.css +6 -2
- package/base/navigation.css +1 -1
- package/base/reset.css +7 -1
- package/base/responsive.css +13 -12
- package/base/shell.css +8 -2
- package/components/_i18n/i18n.js +28 -3
- package/components/_i18n/strings.js +42 -24
- package/components/ai/ChatMessage.d.ts +2 -0
- package/components/ai/ChatMessage.jsx +8 -4
- package/components/ai/ConfidenceMeter.d.ts +2 -0
- package/components/ai/HumanReviewGate.d.ts +2 -0
- package/components/ai/PromptInput.d.ts +2 -0
- package/components/ai/TypingIndicator.d.ts +2 -0
- package/components/ai/ai-conversation.card.html +3 -3
- package/components/ai/ai-lumi.card.html +3 -3
- package/components/ai/ai.card.html +3 -3
- package/components/brand/brand.card.html +3 -3
- package/components/button/buttons.card.html +3 -3
- package/components/data/Image.jsx +18 -2
- package/components/data/Sortable.d.ts +5 -1
- package/components/data/Sortable.jsx +42 -9
- package/components/data/Sortable.prompt.md +4 -2
- package/components/data/Terminal.jsx +3 -2
- package/components/data/Tooltip.d.ts +2 -0
- package/components/data/Tooltip.jsx +6 -3
- package/components/data/Tree.d.ts +2 -1
- package/components/data/Tree.jsx +137 -16
- package/components/data/Tree.prompt.md +1 -1
- package/components/data/collections.card.html +3 -3
- package/components/data/data.card.html +3 -3
- package/components/datatable/DataTable.d.ts +24 -1
- package/components/datatable/DataTable.jsx +78 -5
- package/components/datatable/DataTable.prompt.md +2 -1
- package/components/datatable/datatable.card.html +20 -16
- package/components/dialog/dialog.card.html +3 -3
- package/components/feedback/Alert.d.ts +2 -0
- package/components/feedback/Alert.jsx +8 -2
- package/components/feedback/EmptyState.d.ts +2 -0
- package/components/feedback/EmptyState.jsx +6 -3
- package/components/feedback/ProgressBar.d.ts +2 -0
- package/components/feedback/ProgressBar.jsx +6 -3
- package/components/feedback/Toast.jsx +15 -1
- package/components/feedback/Toast.prompt.md +2 -1
- package/components/feedback/feedback-2.card.html +3 -3
- package/components/feedback/feedback.card.html +3 -3
- package/components/forms/Cascader.jsx +7 -2
- package/components/forms/FileUpload.d.ts +2 -0
- package/components/forms/Form.d.ts +3 -1
- package/components/forms/Form.jsx +9 -1
- package/components/forms/Form.prompt.md +1 -1
- package/components/forms/Mentions.d.ts +1 -1
- package/components/forms/Mentions.jsx +59 -7
- package/components/forms/Mentions.prompt.md +3 -1
- package/components/forms/Rating.d.ts +3 -2
- package/components/forms/Rating.jsx +34 -3
- package/components/forms/Rating.prompt.md +1 -1
- package/components/forms/Select.d.ts +2 -0
- package/components/forms/Select.jsx +6 -2
- package/components/forms/Slider.d.ts +2 -0
- package/components/forms/Slider.jsx +5 -2
- package/components/forms/Switch.d.ts +2 -0
- package/components/forms/Switch.jsx +7 -9
- package/components/forms/Toggle.d.ts +2 -0
- package/components/forms/Toggle.jsx +14 -2
- package/components/forms/forms-2.card.html +3 -3
- package/components/forms/forms.card.html +3 -3
- package/components/icon/Icon.d.ts +1 -1
- package/components/icon/Icon.prompt.md +1 -1
- package/components/icon/icon.card.html +3 -3
- package/components/logo/logo.card.html +3 -3
- package/components/navigation/CommandPalette.d.ts +2 -0
- package/components/navigation/Menu.d.ts +2 -0
- package/components/navigation/Menu.jsx +60 -6
- package/components/navigation/Steps.d.ts +2 -0
- package/components/navigation/Steps.jsx +6 -3
- package/components/navigation/Tabs.d.ts +2 -0
- package/components/navigation/Tabs.jsx +6 -3
- package/components/navigation/Toolbar.d.ts +3 -2
- package/components/navigation/Toolbar.jsx +117 -11
- package/components/navigation/Toolbar.prompt.md +1 -1
- package/components/navigation/navigation.card.html +3 -3
- package/components/navigation/shell.card.html +3 -3
- package/components/overlays/ContextMenu.d.ts +2 -0
- package/components/overlays/ContextMenu.jsx +6 -3
- package/components/overlays/Drawer.d.ts +2 -0
- package/components/overlays/alertdialog.card.html +3 -3
- package/components/overlays/overlays.card.html +3 -3
- package/components/textfield/textfield.card.html +3 -3
- package/dist/styles.min.css +312 -64
- package/docs/ci-cd.md +18 -13
- package/docs/consumer-grant.md +3 -0
- package/docs/consuming.md +7 -7
- package/docs/contrast-report.md +79 -1
- package/docs/conventions.md +17 -4
- package/docs/decisions.md +40 -2
- package/docs/deploy.md +17 -6
- package/docs/design-styles.md +1 -1
- package/docs/quality-gates.md +19 -11
- package/docs/release-runbook.md +78 -0
- package/docs/sync.md +1 -1
- package/docs/vi/ci-cd.md +16 -13
- package/docs/vi/consumer-grant.md +3 -0
- package/docs/vi/consuming.md +7 -7
- package/docs/vi/contrast-report.md +79 -1
- package/docs/vi/conventions.md +18 -5
- package/docs/vi/decisions.md +39 -2
- package/docs/vi/deploy.md +17 -6
- package/docs/vi/quality-gates.md +18 -11
- package/docs/vi/release-runbook.md +78 -0
- package/docs/vi/sync.md +1 -1
- package/docs/viewer.html +2 -2
- package/fonts/OFL.txt +98 -0
- package/guidelines/atomic-view.html +2 -2
- package/guidelines/brand-imagery.html +1 -1
- package/guidelines/elements-variants.html +5 -5
- package/guidelines/spacing-breakpoints.html +2 -1
- package/guidelines/surfaces-glass.html +1 -1
- package/llms.txt +4 -4
- package/package.json +28 -8
- package/templates/app-shell/AppShell.dc.html +3 -3
- package/templates/app-shell/content-schema.json +7 -0
- package/templates/article/Article.dc.html +3 -3
- package/templates/article/content-schema.json +7 -0
- package/templates/auth/Auth.dc.html +26 -10
- package/templates/auth/content-schema.json +25 -0
- package/templates/dashboard/Dashboard.dc.html +4 -4
- package/templates/dashboard/content-schema.json +7 -0
- package/templates/delivery-change-note/DeliveryChangeNote.dc.html +2 -2
- package/templates/delivery-runbook/DeliveryRunbook.dc.html +2 -2
- package/templates/email-safe/email-announcement.html +2 -2
- package/templates/email-safe/email-dunning.html +2 -2
- package/templates/email-safe/email-investor.html +2 -2
- package/templates/email-safe/email-launch.html +2 -2
- package/templates/email-safe/email-newsletter.html +2 -2
- package/templates/email-safe/email-status.html +2 -2
- package/templates/email-safe/email-transactional.html +2 -2
- package/templates/finance-dunning-email/FinanceDunningEmail.dc.html +2 -2
- package/templates/finance-expense-report/FinanceExpenseReport.dc.html +2 -2
- package/templates/finance-invoice/FinanceInvoice.dc.html +2 -2
- package/templates/finance-quote/FinanceQuote.dc.html +4 -4
- package/templates/hr-interview-kit/HrInterviewKit.dc.html +1 -1
- package/templates/marketing-case-study/MarketingCaseStudy.dc.html +1 -1
- package/templates/marketing-page/MarketingPage.dc.html +3 -3
- package/templates/marketing-page/content-schema.json +1 -0
- package/templates/settings/Settings.dc.html +3 -3
- package/templates/settings/content-schema.json +7 -0
- package/tokens/element-seeds.json +2 -2
- package/tokens/elements.css +193 -19
- package/tokens/native/CSTokens.kt +3 -2
- package/tokens/native/CSTokens.swift +3 -2
- package/tokens/native/cs_tokens.dart +3 -2
- package/tokens/provenance.json +8 -8
- package/tokens/spacing.css +2 -1
- package/tokens/tokens.dtcg.json +19 -14
- package/tokens/tokens.js +16 -15
- package/tokens/tokens.json +15 -14
- package/ui_kits/deck/index.html +1 -1
- package/ui_kits/status-hub/StatusHub.jsx +1 -1
- package/ui_kits/status-hub/login.html +52 -10
- package/ui_kits/status-hub/project.html +1 -1
- package/ui_kits/status-hub/settings.html +1 -1
- package/ui_kits/website/Website.jsx +1 -1
- package/ui_kits/website/careers.html +2 -2
- package/ui_kits/website/chat.html +1 -1
- package/ui_kits/website/site.css +1 -1
- package/ui_kits/website/work.html +2 -2
- package/assets/aurora-hoa.png +0 -0
- package/assets/aurora-kim.png +0 -0
- package/assets/aurora-moc.png +0 -0
- package/assets/aurora-thuy.png +0 -0
- package/docs/decisions/container-query-migration.md +0 -18
- package/docs/decisions/overlay-manager-design.md +0 -101
- package/docs/plans/cyberskill-design-system-audit-and-12-month-evolution-plan.md +0 -1490
- package/docs/status/assets/favicon.svg +0 -5
- package/docs/status/assets/status-legacy.css +0 -399
- package/docs/status/assets/status-legacy.js +0 -505
- package/docs/status/assets/status.css +0 -644
- package/docs/status/assets/status.js +0 -1308
- package/docs/status/data/status-feed.json +0 -1
- package/docs/status/data/task/TASK-REL-001.js +0 -1
- package/docs/status/data/task/TASK-REL-002.js +0 -1
- package/docs/status/index.html +0 -46
- package/docs/status/roadmap.html +0 -4
- package/docs/status/status-legacy.html +0 -122
- package/docs/tasks/BACKLOG.md +0 -26
- package/docs/tasks/_state/commit-links.yaml +0 -14
- package/docs/tasks/release/TASK-REL-001-launch-1-1-0/spec.md +0 -55
- package/docs/tasks/release/TASK-REL-002-republish-1-1-1/spec.md +0 -54
|
@@ -9,6 +9,8 @@ export interface FileUploadProps {
|
|
|
9
9
|
multiple?: boolean;
|
|
10
10
|
onFiles?: (files: File[]) => void;
|
|
11
11
|
icon?: ReactNode;
|
|
12
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
13
|
+
lang?: string;
|
|
12
14
|
className?: string;
|
|
13
15
|
}
|
|
14
16
|
export function FileUpload(props: FileUploadProps): React.ReactElement;
|
|
@@ -13,7 +13,9 @@ export interface FormProps {
|
|
|
13
13
|
errors?: Record<string, string | undefined>;
|
|
14
14
|
/** name → rule or rule[]; validated on submit. */
|
|
15
15
|
rules?: Record<string, FormRule | FormRule[]>;
|
|
16
|
-
/** name → async (value, values) => message | null; runs after sync rules pass.
|
|
16
|
+
/** name → async (value, values) => message | null; runs after sync rules pass.
|
|
17
|
+
* While awaiting, Form sets `is-pending` / `aria-busy`, disables controls via CSS,
|
|
18
|
+
* and ignores duplicate submits. Context exposes `pending: boolean`. */
|
|
17
19
|
asyncRules?: Record<string, (value: unknown, values: Record<string, unknown>) => Promise<string | null | undefined>>;
|
|
18
20
|
/** Seed for controller-registered fields. */
|
|
19
21
|
initialValues?: Record<string, unknown>;
|
|
@@ -55,6 +55,7 @@ export function Form({ onSubmit, errors = {}, rules, asyncRules, initialValues,
|
|
|
55
55
|
const [values, setValues] = React.useState(() => initialValues || {});
|
|
56
56
|
const [ruleErrors, setRuleErrors] = React.useState({});
|
|
57
57
|
const [pending, setPending] = React.useState(false);
|
|
58
|
+
const pendingRef = React.useRef(false);
|
|
58
59
|
const setValue = React.useCallback((name, v) => {
|
|
59
60
|
setValues((s) => (String(name).includes(".") ? setPath(s, name, v) : { ...s, [name]: v }));
|
|
60
61
|
setRuleErrors((e) => (e[name] ? { ...e, [name]: undefined } : e));
|
|
@@ -96,20 +97,27 @@ export function Form({ onSubmit, errors = {}, rules, asyncRules, initialValues,
|
|
|
96
97
|
return (
|
|
97
98
|
<Ctx.Provider value={ctx}>
|
|
98
99
|
<form {...props} ref={ref} className={cx("cs-form", pending && "is-pending", className)} noValidate
|
|
100
|
+
aria-busy={pending || undefined}
|
|
99
101
|
onSubmit={async (e) => {
|
|
100
102
|
e.preventDefault();
|
|
103
|
+
// FIND-015 / FIND-044 — lock re-entry while async validation / submit runs.
|
|
104
|
+
if (pendingRef.current) return;
|
|
101
105
|
const fd = new FormData(e.currentTarget);
|
|
102
106
|
const v = {}; fd.forEach((val, k) => { v[k] = val; });
|
|
103
107
|
Object.assign(v, values); // controller-registered fields are the source of truth
|
|
104
108
|
const errs = runRules(v);
|
|
105
109
|
if (Object.keys(errs).some((k) => errs[k])) { setRuleErrors(errs); return; }
|
|
110
|
+
pendingRef.current = true;
|
|
106
111
|
setPending(true);
|
|
107
112
|
try {
|
|
108
113
|
const aerrs = await runAsyncRules(v);
|
|
109
114
|
setRuleErrors(aerrs);
|
|
110
115
|
if (Object.keys(aerrs).some((k) => aerrs[k])) return;
|
|
111
116
|
onSubmit && onSubmit(v);
|
|
112
|
-
} finally {
|
|
117
|
+
} finally {
|
|
118
|
+
pendingRef.current = false;
|
|
119
|
+
setPending(false);
|
|
120
|
+
}
|
|
113
121
|
}}>
|
|
114
122
|
{keys.length ? (
|
|
115
123
|
<div className="cs-form__summary" role="alert">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
Manual mode unchanged: omit `rules`/`name`, pass `errors={{email:"…"}}` yourself.
|
|
12
12
|
|
|
13
|
-
**asyncRules:** optional map of field name → async `(value, values) => message|null`. Runs after sync `rules` pass;
|
|
13
|
+
**asyncRules:** optional map of field name → async `(value, values) => message|null`. Runs after sync `rules` pass; Form emits `is-pending` + `aria-busy`, locks controls, and refuses re-entry until the async pass finishes (FIND-015 / FIND-044). Controllers can also read `pending` from Form context.
|
|
14
14
|
|
|
15
15
|
**FormFieldArray** — manage an array of row objects under `name` on the surrounding Form. Children render-prop: `({ index, item, remove, path })`.
|
|
16
16
|
|
|
@@ -2,28 +2,80 @@ import React from "react";
|
|
|
2
2
|
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
3
3
|
import { cx } from "../_utils/cx.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
let mentionsUid = 0;
|
|
6
|
+
|
|
7
|
+
/** CyberSkill Mentions — textarea that suggests @users while typing (APG editable combobox). */
|
|
6
8
|
export function Mentions({ value, defaultValue = "", onChange, users = [], placeholder, rows = 3, lang, className }) {
|
|
7
9
|
const [inner, setInner] = React.useState(defaultValue);
|
|
8
10
|
const val = value != null ? value : inner;
|
|
9
11
|
const set = (v) => { if (value == null) setInner(v); onChange && onChange(v); };
|
|
10
12
|
const [q, setQ] = React.useState(null);
|
|
13
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
14
|
+
const [listId] = React.useState(() => "cs-mentions-" + (++mentionsUid));
|
|
11
15
|
const [ref, L] = useLang(lang);
|
|
12
16
|
const t = makeT("Mentions", L);
|
|
13
17
|
const ph = placeholder != null ? placeholder : t("placeholder");
|
|
18
|
+
const hits = q == null ? [] : users.filter((u) => u.toLowerCase().includes(q)).slice(0, 6);
|
|
19
|
+
const open = hits.length > 0;
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
setActiveIndex((i) => (hits.length ? Math.min(i, hits.length - 1) : 0));
|
|
23
|
+
}, [q, users]);
|
|
24
|
+
|
|
14
25
|
const onInput = (v) => {
|
|
15
26
|
set(v);
|
|
16
27
|
const m = /(^|\s)@(\w*)$/.exec(v);
|
|
17
|
-
|
|
28
|
+
const next = m ? m[2].toLowerCase() : null;
|
|
29
|
+
setQ(next);
|
|
30
|
+
if (next != null) setActiveIndex(0);
|
|
18
31
|
};
|
|
19
|
-
const hits = q == null ? [] : users.filter((u) => u.toLowerCase().includes(q)).slice(0, 6);
|
|
20
32
|
const pick = (u) => { set(val.replace(/(^|\s)@\w*$/, "$1@" + u + " ")); setQ(null); };
|
|
33
|
+
const optionId = (i) => listId + "-opt-" + i;
|
|
34
|
+
const onKeyDown = (e) => {
|
|
35
|
+
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
36
|
+
if (!open) return;
|
|
37
|
+
if (e.key === "ArrowDown") {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setActiveIndex((i) => Math.min(hits.length - 1, i + 1));
|
|
40
|
+
} else if (e.key === "ArrowUp") {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setActiveIndex((i) => Math.max(0, i - 1));
|
|
43
|
+
} else if (e.key === "Enter") {
|
|
44
|
+
if (hits[activeIndex]) { e.preventDefault(); pick(hits[activeIndex]); }
|
|
45
|
+
} else if (e.key === "Escape") {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setQ(null);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
21
50
|
return (
|
|
22
51
|
<span ref={ref} className={cx("cs-mentions", className)}>
|
|
23
|
-
<textarea
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
<textarea
|
|
53
|
+
className="cs-field__control"
|
|
54
|
+
rows={rows}
|
|
55
|
+
value={val}
|
|
56
|
+
placeholder={ph}
|
|
57
|
+
role="combobox"
|
|
58
|
+
aria-autocomplete="list"
|
|
59
|
+
aria-expanded={open}
|
|
60
|
+
aria-controls={listId}
|
|
61
|
+
aria-activedescendant={open && hits[activeIndex] ? optionId(activeIndex) : undefined}
|
|
62
|
+
onChange={(e) => onInput(e.target.value)}
|
|
63
|
+
onKeyDown={onKeyDown}
|
|
64
|
+
/>
|
|
65
|
+
{open ? (
|
|
66
|
+
<span className="cs-mentions__pop" role="listbox" id={listId}>
|
|
67
|
+
{hits.map((u, i) => (
|
|
68
|
+
<button
|
|
69
|
+
key={u}
|
|
70
|
+
type="button"
|
|
71
|
+
id={optionId(i)}
|
|
72
|
+
role="option"
|
|
73
|
+
aria-selected={i === activeIndex}
|
|
74
|
+
className={cx(i === activeIndex && "hl")}
|
|
75
|
+
onMouseEnter={() => setActiveIndex(i)}
|
|
76
|
+
onMouseDown={(e) => { e.preventDefault(); pick(u); }}
|
|
77
|
+
>@{u}</button>
|
|
78
|
+
))}
|
|
27
79
|
</span>
|
|
28
80
|
) : null}
|
|
29
81
|
</span>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
**Mentions** — textarea suggesting @users as you type.
|
|
1
|
+
**Mentions** — textarea suggesting @users as you type (APG editable combobox).
|
|
2
|
+
|
|
3
|
+
Keyboard: ArrowUp/Down move the active suggestion (`aria-activedescendant`); Enter selects; Escape closes. Mouse click still picks.
|
|
2
4
|
|
|
3
5
|
```jsx
|
|
4
6
|
<Mentions users={["anle","baovu","lumi"]} placeholder="Add a note — @ to mention"/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/** Whole-star rating (radiogroup). Controlled
|
|
2
|
-
* (defaultValue). Clicking the current value
|
|
1
|
+
/** Whole-star rating (APG radiogroup: roving tabindex + arrow keys). Controlled
|
|
2
|
+
* (value/onChange) or uncontrolled (defaultValue). Clicking the current value
|
|
3
|
+
* clears to 0. readOnly for display. */
|
|
3
4
|
export interface RatingProps {
|
|
4
5
|
value?: number;
|
|
5
6
|
defaultValue?: number;
|
|
@@ -3,17 +3,48 @@ import { makeT, useLang } from "../_i18n/i18n.js";
|
|
|
3
3
|
import { cx } from "../_utils/cx.js";
|
|
4
4
|
const STAR = "M12 2l2.9 6.2 6.6.8-4.9 4.6 1.3 6.5L12 16.9 6.1 20l1.3-6.5L2.5 9l6.6-.8z";
|
|
5
5
|
|
|
6
|
-
/** CyberSkill Rating — whole-star rating. Controlled or uncontrolled; readOnly for display. Click the current value to clear. */
|
|
6
|
+
/** CyberSkill Rating — whole-star rating (APG radiogroup: roving tabindex + arrows). Controlled or uncontrolled; readOnly for display. Click the current value to clear. */
|
|
7
7
|
export function Rating({ value, defaultValue = 0, onChange, max = 5, readOnly = false, label, lang, className }) {
|
|
8
8
|
const [inner, setInner] = React.useState(defaultValue);
|
|
9
9
|
const val = value != null ? value : inner;
|
|
10
|
-
const
|
|
10
|
+
const commit = (n) => { if (readOnly) return; if (value == null) setInner(n); onChange && onChange(n); };
|
|
11
|
+
const set = (n) => commit(n === val ? 0 : n);
|
|
11
12
|
const [ref, L] = useLang(lang);
|
|
12
13
|
const lbl = label != null ? label : makeT("Rating", L)("label");
|
|
14
|
+
const refs = React.useRef([]);
|
|
15
|
+
const focusIdx = val > 0 ? Math.min(max, val) - 1 : 0;
|
|
16
|
+
|
|
17
|
+
const move = (from, delta) => {
|
|
18
|
+
if (readOnly) return;
|
|
19
|
+
const next = Math.max(0, Math.min(max - 1, from + delta));
|
|
20
|
+
commit(next + 1);
|
|
21
|
+
const b = refs.current[next];
|
|
22
|
+
if (b) b.focus();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onKeyDown = (e, i) => {
|
|
26
|
+
if (readOnly || e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
27
|
+
if (e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); move(i, 1); }
|
|
28
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") { e.preventDefault(); move(i, -1); }
|
|
29
|
+
else if (e.key === "Home") { e.preventDefault(); move(i, -i); }
|
|
30
|
+
else if (e.key === "End") { e.preventDefault(); move(i, max - 1 - i); }
|
|
31
|
+
};
|
|
32
|
+
|
|
13
33
|
return (
|
|
14
34
|
<div ref={ref} className={cx("cs-rating", className)} role="radiogroup" aria-label={lbl + ": " + val + " / " + max} data-readonly={readOnly ? "true" : undefined}>
|
|
15
35
|
{Array.from({ length: max }).map((_, i) => (
|
|
16
|
-
<button
|
|
36
|
+
<button
|
|
37
|
+
key={i}
|
|
38
|
+
type="button"
|
|
39
|
+
role="radio"
|
|
40
|
+
aria-checked={i < val}
|
|
41
|
+
aria-label={(i + 1) + " / " + max}
|
|
42
|
+
className={i < val ? "on" : undefined}
|
|
43
|
+
onClick={() => set(i + 1)}
|
|
44
|
+
tabIndex={readOnly ? -1 : (i === focusIdx ? 0 : -1)}
|
|
45
|
+
ref={(el) => { refs.current[i] = el; }}
|
|
46
|
+
onKeyDown={(e) => onKeyDown(e, i)}
|
|
47
|
+
>
|
|
17
48
|
<svg width="22" height="22" viewBox="0 0 24 24" fill={i < val ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" aria-hidden="true"><path d={STAR} /></svg>
|
|
18
49
|
</button>
|
|
19
50
|
))}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
**Rating** — whole-star rating (ochre stars). Controlled or uncontrolled; `readOnly` for display; click current value to clear. Bilingual aria via the registry.
|
|
1
|
+
**Rating** — whole-star rating (ochre stars). APG radiogroup: one tab stop, ArrowLeft/Right (and Up/Down) change value. Controlled or uncontrolled; `readOnly` for display; click current value to clear. Bilingual aria via the registry.
|
|
2
2
|
|
|
3
3
|
```jsx
|
|
4
4
|
<Rating defaultValue={4} onChange={setStars}/>
|
|
@@ -14,6 +14,8 @@ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElemen
|
|
|
14
14
|
/** Options as data; alternatively pass <option> children. */
|
|
15
15
|
options?: SelectOption[];
|
|
16
16
|
children?: ReactNode;
|
|
17
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
18
|
+
lang?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function Select(props: SelectProps): React.ReactElement;
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
5
|
/** CyberSkill Select — native <select> in the .cs-field frame with a chevron.
|
|
5
6
|
* Pass `options` [{value,label}] or <option> children. */
|
|
6
|
-
export function Select({ id, label, description, error, options, children, disabled = false, className, ...props }) {
|
|
7
|
+
export function Select({ id, label, description, error, options, children, disabled = false, lang, className, placeholder, ...props }) {
|
|
8
|
+
const [ref, L] = useLang(lang);
|
|
9
|
+
const t = makeT("Select", L);
|
|
7
10
|
const gid = React.useId();
|
|
8
11
|
const sid = id ?? gid;
|
|
9
12
|
const descId = description ? sid + "-desc" : undefined;
|
|
10
13
|
const errId = error ? sid + "-err" : undefined;
|
|
11
14
|
const describedBy = [descId, errId].filter(Boolean).join(" ") || undefined;
|
|
12
15
|
return (
|
|
13
|
-
<label className={cx("cs-field", disabled && "is-disabled", error && "is-invalid", className)} htmlFor={sid}>
|
|
16
|
+
<label ref={ref} className={cx("cs-field", disabled && "is-disabled", error && "is-invalid", className)} htmlFor={sid}>
|
|
14
17
|
{label ? <span className="cs-field__label">{label}</span> : null}
|
|
15
18
|
{description ? <span id={descId} className="cs-field__description">{description}</span> : null}
|
|
16
19
|
<span className="cs-select">
|
|
17
20
|
<select {...props} id={sid} disabled={disabled} aria-invalid={error ? true : undefined} aria-describedby={describedBy} className="cs-field__control">
|
|
21
|
+
{placeholder != null || !children ? <option value="" disabled hidden>{placeholder ?? t("placeholder")}</option> : null}
|
|
18
22
|
{options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}
|
|
19
23
|
</select>
|
|
20
24
|
<span className="cs-select__chevron" aria-hidden="true">
|
|
@@ -4,3 +4,5 @@ import type * as React from "react";
|
|
|
4
4
|
* range props (min, max, step, value, defaultValue, onChange). */
|
|
5
5
|
export interface SliderProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
6
6
|
export function Slider(props: SliderProps): React.ReactElement;
|
|
7
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
8
|
+
lang?: string;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
5
|
/** CyberSkill Slider — brand-tinted native range input. Pass native props (min/max/step/value/onChange). */
|
|
5
6
|
// `children` is destructured but never rendered on purpose: keeps stray children out of {...props} → void <input>.
|
|
6
|
-
export function Slider({ className, children, ...props }) {
|
|
7
|
-
|
|
7
|
+
export function Slider({ className, children, lang, "aria-label": ariaLabel, ...props }) {
|
|
8
|
+
const [ref, L] = useLang(lang);
|
|
9
|
+
const t = makeT("Slider", L);
|
|
10
|
+
return <input ref={ref} type="range" className={cx("cs-slider", className)} aria-label={ariaLabel ?? t("label")} {...props} />;
|
|
8
11
|
}
|
|
@@ -7,6 +7,8 @@ import type { ReactNode } from "react";
|
|
|
7
7
|
*/
|
|
8
8
|
export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
|
|
9
9
|
label?: ReactNode;
|
|
10
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
11
|
+
lang?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function Switch(props: SwitchProps): React.ReactElement;
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
|
-
let warnedNameless = false;
|
|
5
|
-
|
|
6
5
|
/** CyberSkill Switch — accessible toggle (role="switch"); Umber on, Ochre knob-track in dark. */
|
|
7
6
|
// `children` is destructured but never rendered on purpose: keeps stray children out of {...props} → void <input>.
|
|
8
|
-
export function Switch({ label, disabled = false, className, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
|
|
7
|
+
export function Switch({ label, disabled = false, className, children, lang, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
|
|
8
|
+
const [ref, L] = useLang(lang);
|
|
9
|
+
const t = makeT("Switch", L);
|
|
9
10
|
const named = !!(label || ariaLabel || ariaLabelledby);
|
|
10
|
-
|
|
11
|
-
warnedNameless = true;
|
|
12
|
-
console.warn("CyberSkill Switch: render with label, aria-label, or aria-labelledby.");
|
|
13
|
-
}
|
|
11
|
+
const fallback = props.checked || props.defaultChecked ? t("on") : t("off");
|
|
14
12
|
return (
|
|
15
|
-
<label className={cx("cs-switch", disabled && "is-disabled", className)}>
|
|
13
|
+
<label ref={ref} className={cx("cs-switch", disabled && "is-disabled", className)}>
|
|
16
14
|
<input
|
|
17
15
|
type="checkbox"
|
|
18
16
|
role="switch"
|
|
19
17
|
disabled={disabled}
|
|
20
|
-
aria-label={ariaLabel}
|
|
18
|
+
aria-label={ariaLabel ?? (named ? undefined : fallback)}
|
|
21
19
|
aria-labelledby={ariaLabelledby}
|
|
22
20
|
{...props}
|
|
23
21
|
/>
|
|
@@ -11,6 +11,8 @@ export interface ToggleProps {
|
|
|
11
11
|
icon?: ReactNode;
|
|
12
12
|
children?: ReactNode;
|
|
13
13
|
disabled?: boolean;
|
|
14
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
15
|
+
lang?: string;
|
|
14
16
|
className?: string;
|
|
15
17
|
}
|
|
16
18
|
export function Toggle(props: ToggleProps): React.ReactElement;
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
5
|
/** CyberSkill Toggle — a single pressable on/off button (≠ Switch: no form semantics; ≠ SegmentedControl: standalone). */
|
|
5
|
-
export function Toggle({ pressed, defaultPressed = false, onChange, icon, children, disabled = false, className, ...props }) {
|
|
6
|
+
export function Toggle({ pressed, defaultPressed = false, onChange, icon, children, disabled = false, lang, className, "aria-label": ariaLabel, ...props }) {
|
|
6
7
|
const [inner, setInner] = React.useState(defaultPressed);
|
|
7
8
|
const on = pressed != null ? pressed : inner;
|
|
9
|
+
const [ref, L] = useLang(lang);
|
|
10
|
+
const t = makeT("Toggle", L);
|
|
8
11
|
const flip = () => { const v = !on; if (pressed == null) setInner(v); onChange && onChange(v); };
|
|
9
12
|
return (
|
|
10
|
-
<button
|
|
13
|
+
<button
|
|
14
|
+
ref={ref}
|
|
15
|
+
type="button"
|
|
16
|
+
className={cx("cs-toggle", className)}
|
|
17
|
+
aria-pressed={on}
|
|
18
|
+
aria-label={ariaLabel ?? (on ? t("pressed") : t("unpressed"))}
|
|
19
|
+
disabled={disabled}
|
|
20
|
+
onClick={flip}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
11
23
|
{icon ? <span aria-hidden="true" style={{ display: "inline-flex" }}>{icon}</span> : null}
|
|
12
24
|
{children}
|
|
13
25
|
</button>
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
.row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
11
11
|
.lbl { width: 100%; font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); }
|
|
12
12
|
</style>
|
|
13
|
-
<script src="
|
|
14
|
-
<script src="
|
|
15
|
-
<script src="
|
|
13
|
+
<script src="../../_vendor/react/react.production.min.js"></script>
|
|
14
|
+
<script src="../../_vendor/react/react-dom.production.min.js"></script>
|
|
15
|
+
<script src="../../_vendor/babel/babel.min.js"></script>
|
|
16
16
|
<script src="../../_ds_bundle.js"></script>
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
body { margin: 0; background: var(--cs-color-surface-page); padding: 22px 24px; }
|
|
10
10
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px 28px; max-width: 640px; align-items: start; }
|
|
11
11
|
</style>
|
|
12
|
-
<script src="
|
|
13
|
-
<script src="
|
|
14
|
-
<script src="
|
|
12
|
+
<script src="../../_vendor/react/react.production.min.js"></script>
|
|
13
|
+
<script src="../../_vendor/react/react-dom.production.min.js"></script>
|
|
14
|
+
<script src="../../_vendor/babel/babel.min.js"></script>
|
|
15
15
|
<script src="../../_ds_bundle.js"></script>
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -6,7 +6,7 @@ export type IconName =
|
|
|
6
6
|
| "search" | "sliders" | "upload" | "download" | "calendar"
|
|
7
7
|
| "user" | "plus" | "trash" | "external" | "menu"
|
|
8
8
|
| "chevron-down" | "chevron-up" | "chevron-left" | "chevron-right"
|
|
9
|
-
| "edit" | "copy" | "info" | "alert-triangle";
|
|
9
|
+
| "edit" | "copy" | "info" | "alert-triangle" | "pin";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* CyberSkill line-icon renderer. One component drives the whole in-repo icon set
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
<Icon name="sparkle" size="lg" label="Wish" /> {/* meaningful */}
|
|
6
6
|
```
|
|
7
7
|
|
|
8
|
-
Set: `close · sun · moon · arrow-right · check · sparkle · chat · sound-on · sound-off` + v2.0 extension `search · sliders · upload · download · calendar · user · plus · trash · external · menu` + v2.13 `chevron-down/up/left/right · edit · copy · info · alert-triangle` (same grammar). `sparkle` is the brand's "wish" glyph. Colour follows the surrounding text token — set `color` on a parent to recolour.
|
|
8
|
+
Set: `close · sun · moon · arrow-right · check · sparkle · chat · sound-on · sound-off` + v2.0 extension `search · sliders · upload · download · calendar · user · plus · trash · external · menu` + v2.13 `chevron-down/up/left/right · edit · copy · info · alert-triangle · pin` (same grammar). `sparkle` is the brand's "wish" glyph. Colour follows the surrounding text token — set `color` on a parent to recolour.
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
.cell .g { color: var(--cs-color-brand-umber); }
|
|
14
14
|
.cell:nth-child(6) .g { color: var(--cs-color-brand-ochre); }
|
|
15
15
|
</style>
|
|
16
|
-
<script src="
|
|
17
|
-
<script src="
|
|
18
|
-
<script src="
|
|
16
|
+
<script src="../../_vendor/react/react.production.min.js"></script>
|
|
17
|
+
<script src="../../_vendor/react/react-dom.production.min.js"></script>
|
|
18
|
+
<script src="../../_vendor/babel/babel.min.js"></script>
|
|
19
19
|
<script src="../../_ds_bundle.js"></script>
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
.lockup b { font: 800 22px/1 var(--cs-font-family-ui); color: var(--cs-color-brand-umber); letter-spacing: -.01em; }
|
|
16
16
|
.cap { font: 500 12px/1.4 var(--cs-font-family-ui); color: var(--cs-color-text-muted); }
|
|
17
17
|
</style>
|
|
18
|
-
<script src="
|
|
19
|
-
<script src="
|
|
20
|
-
<script src="
|
|
18
|
+
<script src="../../_vendor/react/react.production.min.js"></script>
|
|
19
|
+
<script src="../../_vendor/react/react-dom.production.min.js"></script>
|
|
20
|
+
<script src="../../_vendor/babel/babel.min.js"></script>
|
|
21
21
|
<script src="../../_ds_bundle.js"></script>
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
@@ -14,6 +14,8 @@ export interface CommandPaletteProps {
|
|
|
14
14
|
onClose?: () => void;
|
|
15
15
|
placeholder?: string;
|
|
16
16
|
groups: CommandGroup[];
|
|
17
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
18
|
+
lang?: string;
|
|
17
19
|
className?: string;
|
|
18
20
|
}
|
|
19
21
|
export function CommandPalette(props: CommandPaletteProps): React.ReactElement | null;
|
|
@@ -12,6 +12,8 @@ export interface MenuProps {
|
|
|
12
12
|
align?: "start" | "end";
|
|
13
13
|
open?: boolean;
|
|
14
14
|
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
16
|
+
lang?: string;
|
|
15
17
|
className?: string;
|
|
16
18
|
}
|
|
17
19
|
export function Menu(props: MenuProps): React.ReactElement;
|
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
5
|
/** CyberSkill Menu — dropdown. Provide a `trigger` element; compose MenuItem children. */
|
|
5
|
-
export function Menu({ trigger, children, align = "start", open: controlledOpen, onOpenChange, className }) {
|
|
6
|
+
export function Menu({ trigger, children, align = "start", open: controlledOpen, onOpenChange, lang, className }) {
|
|
6
7
|
const [uOpen, setUOpen] = React.useState(false);
|
|
7
8
|
const open = controlledOpen != null ? controlledOpen : uOpen;
|
|
8
9
|
const set = (v) => { onOpenChange ? onOpenChange(v) : setUOpen(v); };
|
|
9
|
-
const ref =
|
|
10
|
+
const [ref, L] = useLang(lang);
|
|
11
|
+
const t = makeT("Menu", L);
|
|
12
|
+
const triggerRef = React.useRef(null);
|
|
13
|
+
const listRef = React.useRef(null);
|
|
14
|
+
|
|
10
15
|
React.useEffect(() => {
|
|
11
16
|
if (!open) return;
|
|
17
|
+
const items = () => listRef.current
|
|
18
|
+
? [...listRef.current.querySelectorAll('[role="menuitem"]:not([disabled])')]
|
|
19
|
+
: [];
|
|
20
|
+
// FIND-040 — move focus into the menu on open.
|
|
21
|
+
const first = items()[0];
|
|
22
|
+
if (first && first.focus) first.focus();
|
|
23
|
+
|
|
12
24
|
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) set(false); };
|
|
13
|
-
const onKey = (e) => {
|
|
25
|
+
const onKey = (e) => {
|
|
26
|
+
const opts = items();
|
|
27
|
+
if (e.key === "Escape") {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
set(false);
|
|
30
|
+
const el = triggerRef.current;
|
|
31
|
+
if (el && el.focus) el.focus();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!opts.length) return;
|
|
35
|
+
const active = document.activeElement;
|
|
36
|
+
const idx = opts.indexOf(active);
|
|
37
|
+
if (e.key === "ArrowDown") {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
(opts[idx < 0 ? 0 : Math.min(opts.length - 1, idx + 1)] || opts[0]).focus();
|
|
40
|
+
} else if (e.key === "ArrowUp") {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
(opts[idx < 0 ? opts.length - 1 : Math.max(0, idx - 1)] || opts[0]).focus();
|
|
43
|
+
} else if (e.key === "Home") {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
opts[0].focus();
|
|
46
|
+
} else if (e.key === "End") {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
opts[opts.length - 1].focus();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
14
51
|
document.addEventListener("mousedown", onDoc);
|
|
15
52
|
document.addEventListener("keydown", onKey);
|
|
16
|
-
return () => {
|
|
53
|
+
return () => {
|
|
54
|
+
document.removeEventListener("mousedown", onDoc);
|
|
55
|
+
document.removeEventListener("keydown", onKey);
|
|
56
|
+
};
|
|
17
57
|
}, [open]);
|
|
58
|
+
|
|
18
59
|
const toggle = () => set(!open);
|
|
19
60
|
// ARIA attrs must live on the interactive trigger (button), not a wrapping <span>
|
|
20
61
|
// — axe aria-allowed-attr flags aria-haspopup/aria-expanded on generic elements.
|
|
21
62
|
const triggerNode = React.isValidElement(trigger)
|
|
22
63
|
? React.cloneElement(trigger, {
|
|
64
|
+
ref: (node) => {
|
|
65
|
+
triggerRef.current = node;
|
|
66
|
+
const r = trigger.ref;
|
|
67
|
+
if (typeof r === "function") r(node);
|
|
68
|
+
else if (r && typeof r === "object") r.current = node;
|
|
69
|
+
},
|
|
23
70
|
"aria-haspopup": "menu",
|
|
24
71
|
"aria-expanded": open,
|
|
25
72
|
onClick: (e) => {
|
|
@@ -27,12 +74,19 @@ export function Menu({ trigger, children, align = "start", open: controlledOpen,
|
|
|
27
74
|
if (!e.defaultPrevented) toggle();
|
|
28
75
|
},
|
|
29
76
|
})
|
|
30
|
-
: <button type="button" aria-haspopup="menu" aria-expanded={open} onClick={toggle}>{trigger}</button>;
|
|
77
|
+
: <button type="button" ref={triggerRef} aria-haspopup="menu" aria-expanded={open} onClick={toggle}>{trigger}</button>;
|
|
31
78
|
return (
|
|
32
79
|
<div className={cx("cs-menu", className)} ref={ref}>
|
|
33
80
|
{triggerNode}
|
|
34
81
|
{open ? (
|
|
35
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
ref={listRef}
|
|
84
|
+
className={cx("cs-menu__list", align === "end" && "cs-menu__list--end")}
|
|
85
|
+
role="menu"
|
|
86
|
+
aria-label={t("menu")}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
36
90
|
) : null}
|
|
37
91
|
</div>
|
|
38
92
|
);
|
|
@@ -8,6 +8,8 @@ export interface StepItem { title: ReactNode; body?: ReactNode; n?: ReactNode; }
|
|
|
8
8
|
export interface StepsProps {
|
|
9
9
|
steps: StepItem[];
|
|
10
10
|
current?: number;
|
|
11
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
12
|
+
lang?: string;
|
|
11
13
|
className?: string;
|
|
12
14
|
}
|
|
13
15
|
export function Steps(props: StepsProps): React.ReactElement;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { makeT, useLang } from "../_i18n/i18n.js";
|
|
2
3
|
import { cx } from "../_utils/cx.js";
|
|
3
4
|
|
|
4
5
|
/** CyberSkill Steps — horizontal progress stepper. Steps before `current` are done, `current` is active. */
|
|
5
|
-
export function Steps({ steps = [], current = 0, className }) {
|
|
6
|
+
export function Steps({ steps = [], current = 0, lang, className }) {
|
|
7
|
+
const [ref, L] = useLang(lang);
|
|
8
|
+
const t = makeT("Steps", L);
|
|
6
9
|
return (
|
|
7
|
-
<div className={cx("cs-steps", className)}>
|
|
10
|
+
<div ref={ref} className={cx("cs-steps", className)} role="list" aria-label={t("label")}>
|
|
8
11
|
{steps.map((s, i) => {
|
|
9
12
|
const state = i < current ? "done" : i === current ? "active" : "todo";
|
|
10
13
|
return (
|
|
11
|
-
<div key={i} className={cx("cs-step", `cs-step--${state}`)}>
|
|
14
|
+
<div key={i} className={cx("cs-step", `cs-step--${state}`)} role="listitem">
|
|
12
15
|
<span className="cs-step__marker">{state === "done" ? "✓" : s.n || i + 1}</span>
|
|
13
16
|
<span className="cs-step__title">{s.title}</span>
|
|
14
17
|
{s.body ? <span className="cs-step__body">{s.body}</span> : null}
|
|
@@ -11,6 +11,8 @@ export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "o
|
|
|
11
11
|
tabs: TabItem[];
|
|
12
12
|
value?: string;
|
|
13
13
|
onChange?: (value: string) => void;
|
|
14
|
+
/** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
|
|
15
|
+
lang?: string;
|
|
14
16
|
}
|
|
15
17
|
export function Tabs(props: TabsProps): React.ReactElement;
|
|
16
18
|
|