@godxjp/ui 31.29.0 → 31.30.1
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/agent/START-HERE.md +4 -4
- package/agent/components/Legend.json +1 -1
- package/agent/components/PasswordStrength.json +8 -2
- package/agent/components/RangeTimeline.json +28 -1
- package/agent/components.json +37 -4
- package/agent/index.json +4 -4
- package/agent/llms.txt +5 -5
- package/agent/tokens.json +48 -0
- package/dist/components/data-display/legend.d.ts +3 -2
- package/dist/components/data-display/legend.js +10 -1
- package/dist/components/data-display/range-timeline.d.ts +93 -2
- package/dist/components/data-display/range-timeline.js +220 -58
- package/dist/components/data-entry/password-strength.d.ts +22 -5
- package/dist/components/data-entry/password-strength.js +20 -37
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +23 -1
- package/dist/i18n/messages/ja.json +23 -1
- package/dist/i18n/messages/vi.json +23 -1
- package/dist/props/components/data-display.prop.d.ts +9 -0
- package/dist/styles/data-display-layout.css +135 -2
- package/dist/styles/layers.json +1 -1
- package/dist/tokens/components/data-display.css +12 -0
- package/docs/FRAME-COVERAGE-REPORT.md +1 -1
- package/docs/data-display/timeline.tsx +84 -1
- package/docs/i18n/messages/en.json +11 -0
- package/docs/i18n/messages/ja.json +11 -0
- package/docs/i18n/messages/vi.json +11 -0
- package/package.json +2 -2
|
@@ -4,8 +4,18 @@ import * as React from "react";
|
|
|
4
4
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
5
|
import { ChevronDown, ChevronLeft, ChevronRight, GripVertical } from "lucide-react";
|
|
6
6
|
import { Button } from "../general/button.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
|
|
8
|
+
import { CONTRAST_PIVOT, relativeLuminance } from "../../app/tenant-theme.js";
|
|
7
9
|
import { Text } from "../general/typography.js";
|
|
8
10
|
import { cn } from "../../lib/utils.js";
|
|
11
|
+
function inkOn(color) {
|
|
12
|
+
if (!color) return void 0;
|
|
13
|
+
const luminance = relativeLuminance(color);
|
|
14
|
+
if (luminance == null) return void 0;
|
|
15
|
+
return luminance > CONTRAST_PIVOT ? "black" : "white";
|
|
16
|
+
}
|
|
17
|
+
const LABEL_WIDTH_STEP = 8;
|
|
18
|
+
const LABEL_WIDTH_BIG_STEP = 32;
|
|
9
19
|
const RangeTimeline = React.forwardRef(
|
|
10
20
|
function RangeTimeline2({
|
|
11
21
|
label,
|
|
@@ -20,10 +30,30 @@ const RangeTimeline = React.forwardRef(
|
|
|
20
30
|
defaultExpandedValues,
|
|
21
31
|
onExpandedValuesChange,
|
|
22
32
|
sticky = false,
|
|
33
|
+
resizableLabel = false,
|
|
34
|
+
labelWidth,
|
|
35
|
+
defaultLabelWidth,
|
|
36
|
+
onLabelWidthChange,
|
|
37
|
+
busy = false,
|
|
23
38
|
className,
|
|
24
39
|
...props
|
|
25
40
|
}, ref) {
|
|
26
41
|
const { t } = useTranslation();
|
|
42
|
+
const resize = resizableLabel ? {
|
|
43
|
+
min: typeof resizableLabel === "object" && resizableLabel.min || 160,
|
|
44
|
+
max: typeof resizableLabel === "object" && resizableLabel.max || 640
|
|
45
|
+
} : null;
|
|
46
|
+
const [ownLabelWidth, setOwnLabelWidth] = React.useState(defaultLabelWidth);
|
|
47
|
+
const currentLabelWidth = labelWidth ?? ownLabelWidth;
|
|
48
|
+
const labelCell = React.useRef(null);
|
|
49
|
+
const labelDrag = React.useRef(null);
|
|
50
|
+
const setLabelWidth = (next) => {
|
|
51
|
+
if (!resize) return;
|
|
52
|
+
const bounded = Math.round(Math.min(resize.max, Math.max(resize.min, next)));
|
|
53
|
+
if (labelWidth === void 0) setOwnLabelWidth(bounded);
|
|
54
|
+
onLabelWidthChange?.(bounded);
|
|
55
|
+
};
|
|
56
|
+
const measuredLabelWidth = () => currentLabelWidth ?? Math.round(labelCell.current?.getBoundingClientRect().width ?? 0);
|
|
27
57
|
const reactId = React.useId();
|
|
28
58
|
const depthOf = (row) => Math.max(0, Math.floor(row.depth ?? 0));
|
|
29
59
|
const nested = rows.some((row) => depthOf(row) > 0);
|
|
@@ -85,7 +115,7 @@ const RangeTimeline = React.forwardRef(
|
|
|
85
115
|
const row = rows.find((row2) => row2.id === current.id);
|
|
86
116
|
const delta = deltaAt(event.clientX);
|
|
87
117
|
cancel();
|
|
88
|
-
if (!row) return;
|
|
118
|
+
if (!row || row.start === null || row.end === null) return;
|
|
89
119
|
const bounded = current.edge === "start" ? Math.min(delta, row.end - row.start) : Math.max(delta, row.start - row.end);
|
|
90
120
|
if (bounded) onRangeChange?.(row.id, current.edge, bounded);
|
|
91
121
|
};
|
|
@@ -107,7 +137,53 @@ const RangeTimeline = React.forwardRef(
|
|
|
107
137
|
)) })
|
|
108
138
|
] }),
|
|
109
139
|
/* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-header", children: [
|
|
110
|
-
/* @__PURE__ */
|
|
140
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-label", ref: labelCell, children: [
|
|
141
|
+
/* @__PURE__ */ jsx(Text, { weight: "bold", children: label }),
|
|
142
|
+
resize && /* @__PURE__ */ jsx(
|
|
143
|
+
"div",
|
|
144
|
+
{
|
|
145
|
+
role: "separator",
|
|
146
|
+
tabIndex: 0,
|
|
147
|
+
className: "ui-range-timeline-label-resizer",
|
|
148
|
+
"aria-orientation": "vertical",
|
|
149
|
+
"aria-label": t("rangeTimeline.resizeLabel"),
|
|
150
|
+
"aria-valuemin": resize.min,
|
|
151
|
+
"aria-valuemax": resize.max,
|
|
152
|
+
"aria-valuenow": currentLabelWidth ?? void 0,
|
|
153
|
+
onPointerDown: (event) => {
|
|
154
|
+
if (event.button !== 0) return;
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
157
|
+
labelDrag.current = {
|
|
158
|
+
x: event.clientX,
|
|
159
|
+
width: measuredLabelWidth(),
|
|
160
|
+
direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
onPointerMove: (event) => {
|
|
164
|
+
const current = labelDrag.current;
|
|
165
|
+
if (current)
|
|
166
|
+
setLabelWidth(current.width + (event.clientX - current.x) * current.direction);
|
|
167
|
+
},
|
|
168
|
+
onPointerUp: () => labelDrag.current = null,
|
|
169
|
+
onPointerCancel: () => labelDrag.current = null,
|
|
170
|
+
onLostPointerCapture: () => labelDrag.current = null,
|
|
171
|
+
onKeyDown: (event) => {
|
|
172
|
+
const step = event.shiftKey ? LABEL_WIDTH_BIG_STEP : LABEL_WIDTH_STEP;
|
|
173
|
+
const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
|
|
174
|
+
let next = null;
|
|
175
|
+
if (event.key === "ArrowRight") next = measuredLabelWidth() + step * direction;
|
|
176
|
+
else if (event.key === "ArrowLeft")
|
|
177
|
+
next = measuredLabelWidth() - step * direction;
|
|
178
|
+
else if (event.key === "Home") next = resize.min;
|
|
179
|
+
else if (event.key === "End") next = resize.max;
|
|
180
|
+
if (next === null) return;
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
setLabelWidth(next);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
] }),
|
|
111
187
|
/* @__PURE__ */ jsx("div", { className: "ui-range-timeline-columns", ref: track, children: columns.map((column, index) => /* @__PURE__ */ jsx(
|
|
112
188
|
"div",
|
|
113
189
|
{
|
|
@@ -117,7 +193,8 @@ const RangeTimeline = React.forwardRef(
|
|
|
117
193
|
},
|
|
118
194
|
index
|
|
119
195
|
)) })
|
|
120
|
-
] })
|
|
196
|
+
] }),
|
|
197
|
+
busy && /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-busy", "aria-hidden": "true" })
|
|
121
198
|
] });
|
|
122
199
|
const body = /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", role: nested ? "list" : void 0, children: [
|
|
123
200
|
(bordered || columns.some((column) => column.muted)) && // Decorative: the same unit tracks as the header columns, laid once behind every row so
|
|
@@ -136,10 +213,17 @@ const RangeTimeline = React.forwardRef(
|
|
|
136
213
|
] }),
|
|
137
214
|
visibleRows.map((row, index) => {
|
|
138
215
|
const idBase = `${reactId}-row-${rows.indexOf(row)}`;
|
|
216
|
+
const dated = row.start !== null && row.end !== null;
|
|
217
|
+
const rowStart = row.start ?? 0, rowEnd = row.end ?? 0;
|
|
139
218
|
const delta = preview?.id === row.id ? preview.delta : 0;
|
|
140
|
-
const start = Math.min(
|
|
141
|
-
const end = Math.max(
|
|
219
|
+
const start = Math.min(rowEnd, rowStart + (preview?.edge === "start" ? delta : 0));
|
|
220
|
+
const end = Math.max(rowStart, rowEnd + (preview?.edge === "end" ? delta : 0));
|
|
142
221
|
const left = Math.max(0, start), right = Math.min(units, end + 1);
|
|
222
|
+
const plan = dated && row.plan && row.plan.start !== null && row.plan.end !== null ? row.plan : null;
|
|
223
|
+
const planLeft = plan ? Math.max(0, plan.start) : 0, planRight = plan ? Math.min(units, plan.end + 1) : 0;
|
|
224
|
+
const overrun = plan !== null && end > plan.end;
|
|
225
|
+
const early = plan !== null && end < plan.end;
|
|
226
|
+
const overrunLeft = overrun ? Math.max(left, Math.min(units, plan.end + 1)) : 0;
|
|
143
227
|
return /* @__PURE__ */ jsxs(
|
|
144
228
|
"div",
|
|
145
229
|
{
|
|
@@ -182,7 +266,14 @@ const RangeTimeline = React.forwardRef(
|
|
|
182
266
|
/* @__PURE__ */ jsx("div", { id: `${idBase}-label`, className: "ui-range-timeline-label-content", children: row.label })
|
|
183
267
|
]
|
|
184
268
|
}
|
|
185
|
-
) : /* @__PURE__ */ jsx(
|
|
269
|
+
) : /* @__PURE__ */ jsx(
|
|
270
|
+
"div",
|
|
271
|
+
{
|
|
272
|
+
className: "ui-range-timeline-label",
|
|
273
|
+
id: row.tooltip != null ? `${idBase}-label` : void 0,
|
|
274
|
+
children: row.label
|
|
275
|
+
}
|
|
276
|
+
),
|
|
186
277
|
/* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
|
|
187
278
|
today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
|
|
188
279
|
"span",
|
|
@@ -192,71 +283,135 @@ const RangeTimeline = React.forwardRef(
|
|
|
192
283
|
style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
|
|
193
284
|
}
|
|
194
285
|
),
|
|
195
|
-
|
|
286
|
+
!dated && /* @__PURE__ */ jsx("span", { className: "ui-range-timeline-outside", "data-direction": "none", children: /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.noDates") }) }),
|
|
287
|
+
dated && right <= left && (end < 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "before", children: [
|
|
196
288
|
/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true", className: "ui-range-timeline-outside-icon" }),
|
|
197
289
|
/* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideBefore") })
|
|
198
290
|
] }) : /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "after", children: [
|
|
199
291
|
/* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideAfter") }),
|
|
200
292
|
/* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "ui-range-timeline-outside-icon" })
|
|
201
293
|
] })),
|
|
202
|
-
|
|
294
|
+
plan && planRight > planLeft && /* @__PURE__ */ jsx(
|
|
295
|
+
"span",
|
|
296
|
+
{
|
|
297
|
+
"aria-hidden": "true",
|
|
298
|
+
className: "ui-range-timeline-plan",
|
|
299
|
+
style: {
|
|
300
|
+
insetInlineStart: `${planLeft / units * 100}%`,
|
|
301
|
+
inlineSize: `${(planRight - planLeft) / units * 100}%`
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
),
|
|
305
|
+
overrun && right > overrunLeft && /* @__PURE__ */ jsxs(
|
|
306
|
+
"span",
|
|
307
|
+
{
|
|
308
|
+
className: "ui-range-timeline-overrun",
|
|
309
|
+
style: {
|
|
310
|
+
insetInlineStart: `${overrunLeft / units * 100}%`,
|
|
311
|
+
inlineSize: `${(right - overrunLeft) / units * 100}%`
|
|
312
|
+
},
|
|
313
|
+
children: [
|
|
314
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("rangeTimeline.overrun") }),
|
|
315
|
+
row.varianceLabel !== false && /* @__PURE__ */ jsx("span", { className: "ui-range-timeline-variance", children: row.varianceLabel ?? t("rangeTimeline.lateBy", { n: String(end - plan.end) }) })
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
),
|
|
319
|
+
dated && !plan && row.planNote != null && right > left && /* @__PURE__ */ jsx(
|
|
320
|
+
"span",
|
|
321
|
+
{
|
|
322
|
+
className: "ui-range-timeline-plan-note",
|
|
323
|
+
style: { insetInlineStart: `${right / units * 100}%` },
|
|
324
|
+
children: row.planNote
|
|
325
|
+
}
|
|
326
|
+
),
|
|
327
|
+
early && right > left && right <= units && /* @__PURE__ */ jsx(
|
|
328
|
+
"span",
|
|
329
|
+
{
|
|
330
|
+
className: "ui-range-timeline-early",
|
|
331
|
+
style: { insetInlineStart: `${right / units * 100}%` },
|
|
332
|
+
children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("rangeTimeline.early") })
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
dated && right > left && /* @__PURE__ */ jsxs(
|
|
203
336
|
"div",
|
|
204
337
|
{
|
|
205
338
|
className: "ui-range-timeline-bar",
|
|
339
|
+
"data-muted": row.muted ? "true" : void 0,
|
|
340
|
+
"data-overdue": row.overdue ? "true" : void 0,
|
|
206
341
|
style: {
|
|
207
342
|
insetInlineStart: `${left / units * 100}%`,
|
|
208
|
-
inlineSize: `${(right - left) / units * 100}
|
|
343
|
+
inlineSize: `${(right - left) / units * 100}%`,
|
|
344
|
+
...row.color && {
|
|
345
|
+
"--range-timeline-bar-color": row.color,
|
|
346
|
+
"--range-timeline-bar-ink": inkOn(row.color)
|
|
347
|
+
}
|
|
209
348
|
},
|
|
210
|
-
children:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"data-edge": edge,
|
|
219
|
-
"aria-label": edge === "start" ? row.startLabel : row.endLabel,
|
|
220
|
-
onPointerDown: (event) => {
|
|
221
|
-
const width = track.current?.getBoundingClientRect().width ?? 0;
|
|
222
|
-
if (!width || event.button !== 0) return;
|
|
223
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
224
|
-
drag.current = {
|
|
225
|
-
id: row.id,
|
|
226
|
-
edge,
|
|
227
|
-
x: event.clientX,
|
|
228
|
-
width,
|
|
229
|
-
direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
|
|
230
|
-
};
|
|
231
|
-
},
|
|
232
|
-
onPointerMove: (event) => {
|
|
233
|
-
if (drag.current)
|
|
234
|
-
setPreview({
|
|
235
|
-
id: drag.current.id,
|
|
236
|
-
edge: drag.current.edge,
|
|
237
|
-
delta: deltaAt(event.clientX)
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
onPointerUp: complete,
|
|
241
|
-
onPointerCancel: cancel,
|
|
242
|
-
onLostPointerCapture: cancel,
|
|
243
|
-
onKeyDown: (event) => {
|
|
244
|
-
if (event.key === "Escape") {
|
|
245
|
-
cancel();
|
|
246
|
-
return;
|
|
349
|
+
children: [
|
|
350
|
+
row.tooltip != null && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
351
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
352
|
+
"span",
|
|
353
|
+
{
|
|
354
|
+
className: "ui-range-timeline-bar-hit ui-focus-ring",
|
|
355
|
+
tabIndex: 0,
|
|
356
|
+
"aria-labelledby": `${idBase}-label`
|
|
247
357
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
358
|
+
) }),
|
|
359
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: row.tooltip })
|
|
360
|
+
] }),
|
|
361
|
+
row.overdue && /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("rangeTimeline.overdue") }),
|
|
362
|
+
onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
|
|
363
|
+
(edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
|
|
364
|
+
).filter(
|
|
365
|
+
(edge) => row.editable === void 0 || row.editable === true ? true : row.editable === false ? false : row.editable[edge] !== false
|
|
366
|
+
).map((edge) => /* @__PURE__ */ jsx(
|
|
367
|
+
Button,
|
|
368
|
+
{
|
|
369
|
+
variant: "ghost",
|
|
370
|
+
size: "icon-xs",
|
|
371
|
+
className: "ui-range-timeline-handle",
|
|
372
|
+
"data-edge": edge,
|
|
373
|
+
"aria-label": edge === "start" ? row.startLabel : row.endLabel,
|
|
374
|
+
onPointerDown: (event) => {
|
|
375
|
+
const width = track.current?.getBoundingClientRect().width ?? 0;
|
|
376
|
+
if (!width || event.button !== 0) return;
|
|
377
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
378
|
+
drag.current = {
|
|
379
|
+
id: row.id,
|
|
380
|
+
edge,
|
|
381
|
+
x: event.clientX,
|
|
382
|
+
width,
|
|
383
|
+
direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
onPointerMove: (event) => {
|
|
387
|
+
if (drag.current)
|
|
388
|
+
setPreview({
|
|
389
|
+
id: drag.current.id,
|
|
390
|
+
edge: drag.current.edge,
|
|
391
|
+
delta: deltaAt(event.clientX)
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
onPointerUp: complete,
|
|
395
|
+
onPointerCancel: cancel,
|
|
396
|
+
onLostPointerCapture: cancel,
|
|
397
|
+
onKeyDown: (event) => {
|
|
398
|
+
if (event.key === "Escape") {
|
|
399
|
+
cancel();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
|
|
403
|
+
if (!step) return;
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
|
|
406
|
+
const delta2 = step * direction;
|
|
407
|
+
if (edge === "start" ? rowStart + delta2 <= rowEnd : rowEnd + delta2 >= rowStart)
|
|
408
|
+
onRangeChange(row.id, edge, delta2);
|
|
409
|
+
},
|
|
410
|
+
children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
|
|
255
411
|
},
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
))
|
|
412
|
+
edge
|
|
413
|
+
))
|
|
414
|
+
]
|
|
260
415
|
}
|
|
261
416
|
)
|
|
262
417
|
] })
|
|
@@ -274,7 +429,14 @@ const RangeTimeline = React.forwardRef(
|
|
|
274
429
|
// Only a NON-default step scopes the unit-width knob, so a theme that narrows
|
|
275
430
|
// --range-timeline-unit-width globally still owns the default timeline (gh#730).
|
|
276
431
|
"data-density": density === "default" ? void 0 : density,
|
|
277
|
-
"
|
|
432
|
+
"data-busy": busy ? "true" : void 0,
|
|
433
|
+
"aria-busy": busy || void 0,
|
|
434
|
+
"aria-label": label,
|
|
435
|
+
// Only once a width exists does it override the token, so a theme's width stays the default.
|
|
436
|
+
style: currentLabelWidth !== void 0 ? {
|
|
437
|
+
...props.style,
|
|
438
|
+
"--range-timeline-label-width": `${currentLabelWidth}px`
|
|
439
|
+
} : props.style
|
|
278
440
|
};
|
|
279
441
|
if (!sticky) {
|
|
280
442
|
return /* @__PURE__ */ jsx("section", { ...sectionProps, tabIndex: 0, children: /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-canvas", style: canvasStyle, children: [
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
declare const DEFAULT_PASSWORD_RULES: readonly ["length", "upper", "lower", "number", "symbol"];
|
|
2
2
|
export type PasswordRule = (typeof DEFAULT_PASSWORD_RULES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* Every visible and screen-reader string, each optional (gh#1191). Unset entries come from the
|
|
5
|
+
* kit's own ja / en / vi catalogue for the active locale, so most apps pass nothing.
|
|
6
|
+
*/
|
|
3
7
|
export type PasswordStrengthLabels = {
|
|
4
|
-
weak
|
|
5
|
-
fair
|
|
6
|
-
strong
|
|
8
|
+
weak?: string;
|
|
9
|
+
fair?: string;
|
|
10
|
+
strong?: string;
|
|
11
|
+
/** Checklist line per rule. */
|
|
12
|
+
rules?: Partial<Record<PasswordRule, string>>;
|
|
13
|
+
/** Screen-reader prefix before a met rule (e.g. "Passed: "). */
|
|
14
|
+
passed?: string;
|
|
15
|
+
/** Screen-reader prefix before an unmet rule (e.g. "Not yet: "). */
|
|
16
|
+
failed?: string;
|
|
17
|
+
/** The polite live-region sentence, given the strength word. */
|
|
18
|
+
announcement?: (strength: string) => string;
|
|
7
19
|
};
|
|
8
20
|
export type PasswordStrengthProps = {
|
|
9
21
|
value: string;
|
|
22
|
+
/**
|
|
23
|
+
* The length rule's threshold (gh#1193), so the checklist matches the server's policy, e.g. 12 for
|
|
24
|
+
* Laravel `Password::min(12)`. Default 8. It drives the check, the score and the rule text.
|
|
25
|
+
*/
|
|
26
|
+
minLength?: number;
|
|
10
27
|
rules?: PasswordRule[];
|
|
11
28
|
showChecklist?: boolean;
|
|
12
29
|
labels?: PasswordStrengthLabels;
|
|
@@ -15,6 +32,6 @@ export type PasswordStrengthReturn = {
|
|
|
15
32
|
score: 0 | 1 | 2 | 3 | 4;
|
|
16
33
|
checks: Record<PasswordRule, boolean>;
|
|
17
34
|
};
|
|
18
|
-
export declare function usePasswordStrength(value: string, rules?: PasswordRule[]): PasswordStrengthReturn;
|
|
19
|
-
export declare function PasswordStrength({ value, rules, showChecklist, labels, }: PasswordStrengthProps): import("react").JSX.Element;
|
|
35
|
+
export declare function usePasswordStrength(value: string, rules?: PasswordRule[], minLength?: number): PasswordStrengthReturn;
|
|
36
|
+
export declare function PasswordStrength({ value, minLength, rules, showChecklist, labels, }: PasswordStrengthProps): import("react").JSX.Element;
|
|
20
37
|
export {};
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, X } from "lucide-react";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
4
5
|
const DEFAULT_PASSWORD_RULES = ["length", "upper", "lower", "number", "symbol"];
|
|
5
|
-
|
|
6
|
-
weak: "Weak",
|
|
7
|
-
fair: "Fair",
|
|
8
|
-
strong: "Strong"
|
|
9
|
-
};
|
|
10
|
-
function usePasswordStrength(value, rules = [...DEFAULT_PASSWORD_RULES]) {
|
|
6
|
+
function usePasswordStrength(value, rules = [...DEFAULT_PASSWORD_RULES], minLength = 8) {
|
|
11
7
|
const uniqueRules = [...new Set(rules)];
|
|
12
8
|
const checks = {
|
|
13
|
-
length: value.length >=
|
|
9
|
+
length: value.length >= minLength,
|
|
14
10
|
upper: /[A-Z]/.test(value),
|
|
15
11
|
lower: /[a-z]/.test(value),
|
|
16
12
|
number: /\d/.test(value),
|
|
@@ -25,28 +21,34 @@ function scoreTone(score) {
|
|
|
25
21
|
if (score <= 3) return "warning";
|
|
26
22
|
return "success";
|
|
27
23
|
}
|
|
28
|
-
function
|
|
29
|
-
if (score <= 1) return
|
|
30
|
-
if (score <= 3) return
|
|
31
|
-
return
|
|
24
|
+
function scoreKey(score) {
|
|
25
|
+
if (score <= 1) return "weak";
|
|
26
|
+
if (score <= 3) return "fair";
|
|
27
|
+
return "strong";
|
|
32
28
|
}
|
|
33
29
|
function PasswordStrength({
|
|
34
30
|
value,
|
|
31
|
+
minLength = 8,
|
|
35
32
|
rules = [...DEFAULT_PASSWORD_RULES],
|
|
36
33
|
showChecklist = true,
|
|
37
|
-
labels =
|
|
34
|
+
labels = {}
|
|
38
35
|
}) {
|
|
36
|
+
const { t } = useTranslation();
|
|
39
37
|
const normalizedRules = [...new Set(rules)];
|
|
40
|
-
const { score, checks } = usePasswordStrength(value, normalizedRules);
|
|
38
|
+
const { score, checks } = usePasswordStrength(value, normalizedRules, minLength);
|
|
41
39
|
const tone = scoreTone(score);
|
|
42
40
|
const segments = Array.from({ length: 5 });
|
|
41
|
+
const key = scoreKey(score);
|
|
42
|
+
const strength = labels[key] ?? t(`dataEntry.passwordStrength.${key}`);
|
|
43
|
+
const ruleLabel = (rule) => labels.rules?.[rule] ?? t(`dataEntry.passwordStrength.rules.${rule}`, { n: String(minLength) });
|
|
44
|
+
const announcement = labels.announcement ? labels.announcement(strength) : t("dataEntry.passwordStrength.announcement", { strength });
|
|
43
45
|
return /* @__PURE__ */ jsxs("div", { className: "ui-password-strength", children: [
|
|
44
46
|
/* @__PURE__ */ jsx(
|
|
45
47
|
"div",
|
|
46
48
|
{
|
|
47
49
|
className: "ui-password-strength-track",
|
|
48
50
|
role: "img",
|
|
49
|
-
"aria-label":
|
|
51
|
+
"aria-label": t("dataEntry.passwordStrength.meter", { score: String(score) }),
|
|
50
52
|
children: segments.map((_, index) => {
|
|
51
53
|
const filled = index < score + 1;
|
|
52
54
|
return /* @__PURE__ */ jsx(
|
|
@@ -63,7 +65,7 @@ function PasswordStrength({
|
|
|
63
65
|
}
|
|
64
66
|
),
|
|
65
67
|
/* @__PURE__ */ jsxs("div", { className: "ui-password-strength-meta", children: [
|
|
66
|
-
/* @__PURE__ */ jsx("span", { className: "ui-password-strength-label", children:
|
|
68
|
+
/* @__PURE__ */ jsx("span", { className: "ui-password-strength-label", children: strength }),
|
|
67
69
|
/* @__PURE__ */ jsxs("span", { className: "ui-password-strength-score", "aria-hidden": "true", children: [
|
|
68
70
|
score,
|
|
69
71
|
"/4"
|
|
@@ -76,34 +78,15 @@ function PasswordStrength({
|
|
|
76
78
|
"data-state": checks[rule] ? "passed" : "failed",
|
|
77
79
|
children: [
|
|
78
80
|
checks[rule] ? /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
|
|
79
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: checks[rule] ?
|
|
80
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
81
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: checks[rule] ? labels.passed ?? t("dataEntry.passwordStrength.passed") : labels.failed ?? t("dataEntry.passwordStrength.failed") }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { children: ruleLabel(rule) })
|
|
81
83
|
]
|
|
82
84
|
},
|
|
83
85
|
rule
|
|
84
86
|
)) }) : null,
|
|
85
|
-
/* @__PURE__ */
|
|
86
|
-
scoreLabel(score, labels),
|
|
87
|
-
" password strength"
|
|
88
|
-
] })
|
|
87
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": "polite", children: announcement })
|
|
89
88
|
] });
|
|
90
89
|
}
|
|
91
|
-
function labelForRule(rule) {
|
|
92
|
-
switch (rule) {
|
|
93
|
-
case "length":
|
|
94
|
-
return "8+ characters";
|
|
95
|
-
case "upper":
|
|
96
|
-
return "Contains uppercase letter";
|
|
97
|
-
case "lower":
|
|
98
|
-
return "Contains lowercase letter";
|
|
99
|
-
case "number":
|
|
100
|
-
return "Contains number";
|
|
101
|
-
case "symbol":
|
|
102
|
-
return "Contains symbol";
|
|
103
|
-
default:
|
|
104
|
-
return rule;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
90
|
export {
|
|
108
91
|
PasswordStrength,
|
|
109
92
|
usePasswordStrength
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.30.1",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -240,6 +240,22 @@
|
|
|
240
240
|
"moved": "{label}: position {position} of {count}.",
|
|
241
241
|
"dropped": "Dropped {label} at position {position} of {count}.",
|
|
242
242
|
"cancelled": "Move cancelled. {label} is back at position {position} of {count}."
|
|
243
|
+
},
|
|
244
|
+
"passwordStrength": {
|
|
245
|
+
"weak": "Weak",
|
|
246
|
+
"fair": "Fair",
|
|
247
|
+
"strong": "Strong",
|
|
248
|
+
"meter": "Password strength {score} of 4",
|
|
249
|
+
"passed": "Passed: ",
|
|
250
|
+
"failed": "Not yet: ",
|
|
251
|
+
"announcement": "Password strength: {strength}",
|
|
252
|
+
"rules": {
|
|
253
|
+
"length": "{n}+ characters",
|
|
254
|
+
"upper": "Contains uppercase letter",
|
|
255
|
+
"lower": "Contains lowercase letter",
|
|
256
|
+
"number": "Contains number",
|
|
257
|
+
"symbol": "Contains symbol"
|
|
258
|
+
}
|
|
243
259
|
}
|
|
244
260
|
},
|
|
245
261
|
"feedback": {
|
|
@@ -749,7 +765,13 @@
|
|
|
749
765
|
"childRows": "Child rows",
|
|
750
766
|
"outsideAfter": "After the visible range",
|
|
751
767
|
"outsideBefore": "Before the visible range",
|
|
752
|
-
"schedule": "Schedule"
|
|
768
|
+
"schedule": "Schedule",
|
|
769
|
+
"noDates": "No dates",
|
|
770
|
+
"resizeLabel": "Resize the label column",
|
|
771
|
+
"overdue": "Overdue",
|
|
772
|
+
"overrun": "Over the plan",
|
|
773
|
+
"early": "Ahead of the plan",
|
|
774
|
+
"lateBy": "+{n}d"
|
|
753
775
|
},
|
|
754
776
|
"chat": {
|
|
755
777
|
"bubble": {
|
|
@@ -236,6 +236,22 @@
|
|
|
236
236
|
"moved": "{label}: {count} 件中 {position} 番目。",
|
|
237
237
|
"dropped": "{label} を {count} 件中 {position} 番目に置きました。",
|
|
238
238
|
"cancelled": "移動を取り消しました。{label} は {count} 件中 {position} 番目に戻りました。"
|
|
239
|
+
},
|
|
240
|
+
"passwordStrength": {
|
|
241
|
+
"weak": "弱い",
|
|
242
|
+
"fair": "普通",
|
|
243
|
+
"strong": "強い",
|
|
244
|
+
"meter": "パスワードの強度 4 段階中 {score}",
|
|
245
|
+
"passed": "達成: ",
|
|
246
|
+
"failed": "未達成: ",
|
|
247
|
+
"announcement": "パスワードの強度: {strength}",
|
|
248
|
+
"rules": {
|
|
249
|
+
"length": "{n} 文字以上",
|
|
250
|
+
"upper": "大文字を含む",
|
|
251
|
+
"lower": "小文字を含む",
|
|
252
|
+
"number": "数字を含む",
|
|
253
|
+
"symbol": "記号を含む"
|
|
254
|
+
}
|
|
239
255
|
}
|
|
240
256
|
},
|
|
241
257
|
"feedback": {
|
|
@@ -724,7 +740,13 @@
|
|
|
724
740
|
"childRows": "子の行",
|
|
725
741
|
"outsideAfter": "表示範囲より後",
|
|
726
742
|
"outsideBefore": "表示範囲より前",
|
|
727
|
-
"schedule": "スケジュール"
|
|
743
|
+
"schedule": "スケジュール",
|
|
744
|
+
"noDates": "日付なし",
|
|
745
|
+
"resizeLabel": "ラベル列の幅を変更",
|
|
746
|
+
"overdue": "期限超過",
|
|
747
|
+
"overrun": "予定超過",
|
|
748
|
+
"early": "予定より早く完了",
|
|
749
|
+
"lateBy": "+{n}日"
|
|
728
750
|
},
|
|
729
751
|
"chat": {
|
|
730
752
|
"bubble": {
|
|
@@ -237,6 +237,22 @@
|
|
|
237
237
|
"moved": "{label}: vị trí {position}/{count}.",
|
|
238
238
|
"dropped": "Đã thả {label} ở vị trí {position}/{count}.",
|
|
239
239
|
"cancelled": "Đã huỷ di chuyển. {label} trở lại vị trí {position}/{count}."
|
|
240
|
+
},
|
|
241
|
+
"passwordStrength": {
|
|
242
|
+
"weak": "Yếu",
|
|
243
|
+
"fair": "Trung bình",
|
|
244
|
+
"strong": "Mạnh",
|
|
245
|
+
"meter": "Độ mạnh mật khẩu {score} trên 4",
|
|
246
|
+
"passed": "Đạt: ",
|
|
247
|
+
"failed": "Chưa đạt: ",
|
|
248
|
+
"announcement": "Độ mạnh mật khẩu: {strength}",
|
|
249
|
+
"rules": {
|
|
250
|
+
"length": "Tối thiểu {n} ký tự",
|
|
251
|
+
"upper": "Có chữ in hoa",
|
|
252
|
+
"lower": "Có chữ thường",
|
|
253
|
+
"number": "Có chữ số",
|
|
254
|
+
"symbol": "Có ký hiệu"
|
|
255
|
+
}
|
|
240
256
|
}
|
|
241
257
|
},
|
|
242
258
|
"feedback": {
|
|
@@ -725,7 +741,13 @@
|
|
|
725
741
|
"childRows": "Hàng con",
|
|
726
742
|
"outsideAfter": "Sau khoảng đang xem",
|
|
727
743
|
"outsideBefore": "Trước khoảng đang xem",
|
|
728
|
-
"schedule": "Lịch công việc"
|
|
744
|
+
"schedule": "Lịch công việc",
|
|
745
|
+
"noDates": "Chưa có ngày",
|
|
746
|
+
"resizeLabel": "Đổi độ rộng cột nhãn",
|
|
747
|
+
"overdue": "Quá hạn",
|
|
748
|
+
"overrun": "Vượt kế hoạch",
|
|
749
|
+
"early": "Sớm hơn kế hoạch",
|
|
750
|
+
"lateBy": "+{n} ngày"
|
|
729
751
|
},
|
|
730
752
|
"chat": {
|
|
731
753
|
"bubble": {
|
|
@@ -71,6 +71,15 @@ import type { TreeFieldNamesProp, TreeOptionProp } from "./data-entry.prop.js";
|
|
|
71
71
|
export type LegendItemProp = {
|
|
72
72
|
/** The tone this key explains — the SAME tone the marks it stands for are drawn in. */
|
|
73
73
|
tone: ToneProp;
|
|
74
|
+
color?: never;
|
|
75
|
+
label: LabelProp;
|
|
76
|
+
} | {
|
|
77
|
+
/**
|
|
78
|
+
* A hex colour from data (the same contract as `Badge color` / `RangeTimelineRow color`),
|
|
79
|
+
* for keys whose colours are user-chosen, e.g. a project's status colours (gh#1189).
|
|
80
|
+
*/
|
|
81
|
+
color: string;
|
|
82
|
+
tone?: never;
|
|
74
83
|
label: LabelProp;
|
|
75
84
|
};
|
|
76
85
|
/** @see Legend — the key for a colour-coded surface: a breakdown bar, a chart, a status column. */
|