@eifi1/ui-kit 0.6.0 → 0.7.0
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/README.md +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +36 -7
- package/dist/components/autocomplete.js +68 -22
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +7 -2
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +36 -5
- package/dist/components/file-button.js +42 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +9 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +9 -0
- package/dist/components/switch.js +7 -2
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +31 -7
- package/dist/components/ui.js +26 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +43 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +43 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +43 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +43 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +43 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +43 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +43 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +133 -84
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +123 -33
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/checkbox.tsx +16 -0
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +102 -6
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/switch.tsx +16 -0
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +74 -30
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +200 -129
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { formatFileSize } from "../kit-labels";
|
|
2
|
+
import type { UiKitLabels } from "../kit-labels";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The kit's words in Chinese (Simplified): every namespace of {@link UiKitLabels}, for
|
|
6
|
+
* `<UiKitProvider labels={UI_KIT_LABELS_ZH}>`.
|
|
7
|
+
*
|
|
8
|
+
* Self-contained on purpose — nothing but the kit's own `formatFileSize` — so it works
|
|
9
|
+
* as a starting point to copy and adjust. Counts and sizes are formatted with
|
|
10
|
+
* `zh-CN` digits; {@link uiKitLabelsZh} takes another number locale
|
|
11
|
+
* (e.g. `"de-CH"` for 1’234) without touching the words.
|
|
12
|
+
*/
|
|
13
|
+
export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
14
|
+
const num = new Intl.NumberFormat(numberLocale);
|
|
15
|
+
const n = (value: number) => num.format(value);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
feedbackAttachment: {
|
|
19
|
+
attachmentAdd: "附加图片",
|
|
20
|
+
attachmentCapture: "截取屏幕",
|
|
21
|
+
attachmentPaste: "……或从剪贴板粘贴截图。",
|
|
22
|
+
attachmentRemove: "移除附件",
|
|
23
|
+
},
|
|
24
|
+
measuredGrid: {
|
|
25
|
+
view: "表格视图",
|
|
26
|
+
cellsView: "单元格",
|
|
27
|
+
textView: "文本",
|
|
28
|
+
addRow: "添加行",
|
|
29
|
+
removeRow: (row) => `删除第 ${row} 行`,
|
|
30
|
+
clear: "清空表格",
|
|
31
|
+
pasteHint: "可将电子表格中的一块数据粘贴到任意单元格",
|
|
32
|
+
cell: (column, row) => `${column},第 ${row} 行`,
|
|
33
|
+
rowNumber: "行",
|
|
34
|
+
rowActions: "行操作",
|
|
35
|
+
keyboardHint:
|
|
36
|
+
"方向键在单元格之间移动。直接输入替换单元格内容,F2 编辑,Esc 撤销编辑。回车向下移动,并在末尾添加一行。",
|
|
37
|
+
lineError: (line) => `无法读取第 ${line} 行`,
|
|
38
|
+
points: (count) => `${count} 个点`,
|
|
39
|
+
problems: (count) => `${count} 个单元格不是数字`,
|
|
40
|
+
},
|
|
41
|
+
pageContents: { title: "本页目录" },
|
|
42
|
+
common: {
|
|
43
|
+
close: "关闭",
|
|
44
|
+
clear: "清除",
|
|
45
|
+
search: "搜索",
|
|
46
|
+
done: "完成",
|
|
47
|
+
cancel: "取消",
|
|
48
|
+
save: "保存",
|
|
49
|
+
back: "上一步",
|
|
50
|
+
next: "下一步",
|
|
51
|
+
remove: "移除",
|
|
52
|
+
loading: "加载中…",
|
|
53
|
+
noResults: "无结果",
|
|
54
|
+
// Full-width colon, no space after it.
|
|
55
|
+
fieldValue: (field, value) => `${field}:${value}`,
|
|
56
|
+
},
|
|
57
|
+
dataTable: {
|
|
58
|
+
columns: "列",
|
|
59
|
+
selectAllRows: "全选所有行",
|
|
60
|
+
sortHint: "点击排序 · 按住 Shift 点击可添加排序",
|
|
61
|
+
filter: "筛选",
|
|
62
|
+
close: "关闭",
|
|
63
|
+
selectRow: "选择行",
|
|
64
|
+
autoSize: "自动调整列宽",
|
|
65
|
+
loading: "加载中…",
|
|
66
|
+
filters: "筛选条件",
|
|
67
|
+
clearAll: "全部清除",
|
|
68
|
+
done: "完成",
|
|
69
|
+
pageSize: "每页行数",
|
|
70
|
+
pageSizeAll: "全部",
|
|
71
|
+
prevPage: "上一页",
|
|
72
|
+
nextPage: "下一页",
|
|
73
|
+
clearFilter: "清除筛选",
|
|
74
|
+
filterPlaceholder: "筛选…",
|
|
75
|
+
selectFilter: "选择",
|
|
76
|
+
selectAll: "全部",
|
|
77
|
+
selectNone: "无",
|
|
78
|
+
dateFrom: "从",
|
|
79
|
+
dateTo: "至",
|
|
80
|
+
numberMin: "最小值",
|
|
81
|
+
numberMax: "最大值",
|
|
82
|
+
numberAbs: "绝对值",
|
|
83
|
+
presets: {
|
|
84
|
+
today: "今天",
|
|
85
|
+
yesterday: "昨天",
|
|
86
|
+
this_week: "本周",
|
|
87
|
+
last_week: "上周",
|
|
88
|
+
last_7_days: "最近 7 天",
|
|
89
|
+
last_30_days: "最近 30 天",
|
|
90
|
+
this_month: "本月",
|
|
91
|
+
last_month: "上月",
|
|
92
|
+
last_3_months: "最近 3 个月",
|
|
93
|
+
ytd: "今年至今",
|
|
94
|
+
last_year: "去年",
|
|
95
|
+
},
|
|
96
|
+
table: "数据表格",
|
|
97
|
+
filterResults: (shown, total) => `显示 ${n(shown)} 行,共 ${n(total)} 行`,
|
|
98
|
+
sortedAscending: (column) => `已按“${column}”升序排序`,
|
|
99
|
+
sortedDescending: (column) => `已按“${column}”降序排序`,
|
|
100
|
+
sortCleared: (column) => `已取消“${column}”的排序`,
|
|
101
|
+
pageChanged: (page, totalPages) => `第 ${n(page)} 页,共 ${n(totalPages)} 页`,
|
|
102
|
+
pageRange: (from, to, total) => `${n(from)}–${n(to)} / ${n(total)}`,
|
|
103
|
+
rowCount: (total) => n(total),
|
|
104
|
+
columnsCount: (visible, total) => `列(${n(visible)}/${n(total)})`,
|
|
105
|
+
},
|
|
106
|
+
miniCalendar: {
|
|
107
|
+
previousMonth: "上个月",
|
|
108
|
+
nextMonth: "下个月",
|
|
109
|
+
// Already formatted in the provider's locale ("2026年9月14日星期一").
|
|
110
|
+
day: (date) => date,
|
|
111
|
+
chooseStart: "请选择开始日期",
|
|
112
|
+
chooseEnd: "请选择结束日期",
|
|
113
|
+
startSelected: (date) => `已选择 ${date} 为开始日期。请选择结束日期。`,
|
|
114
|
+
rangeSelected: (from, to) => `已选择 ${from} 至 ${to}。如需重新选择,请先选择开始日期。`,
|
|
115
|
+
},
|
|
116
|
+
monthPicker: {
|
|
117
|
+
previousYear: "上一年",
|
|
118
|
+
nextYear: "下一年",
|
|
119
|
+
panel: "选择月份",
|
|
120
|
+
month: (monthYear) => monthYear,
|
|
121
|
+
},
|
|
122
|
+
datePicker: {
|
|
123
|
+
panel: "选择日期",
|
|
124
|
+
rangePanel: "选择日期范围",
|
|
125
|
+
clear: "清除",
|
|
126
|
+
previousDay: "前一天",
|
|
127
|
+
nextDay: "后一天",
|
|
128
|
+
today: "今天",
|
|
129
|
+
},
|
|
130
|
+
popover: {
|
|
131
|
+
panel: "弹出框",
|
|
132
|
+
},
|
|
133
|
+
combobox: {
|
|
134
|
+
search: "搜索",
|
|
135
|
+
noResults: "无结果",
|
|
136
|
+
clear: "清除",
|
|
137
|
+
loading: "加载中…",
|
|
138
|
+
create: (query) => `创建“${query}”`,
|
|
139
|
+
selectedCount: (count) => `已选 ${n(count)} 项`,
|
|
140
|
+
loadError: "无法加载结果",
|
|
141
|
+
resultCount: (count) => `${n(count)} 个结果`,
|
|
142
|
+
minChars: (count) => `请至少输入 ${n(count)} 个字符`,
|
|
143
|
+
},
|
|
144
|
+
multiSelect: {
|
|
145
|
+
search: "搜索",
|
|
146
|
+
selectAll: "全选",
|
|
147
|
+
clear: "清除",
|
|
148
|
+
all: "全部",
|
|
149
|
+
// The bare count, as in English: the trigger has always shown just the number.
|
|
150
|
+
selectedCount: (count) => n(count),
|
|
151
|
+
},
|
|
152
|
+
calculator: {
|
|
153
|
+
open: "打开计算器",
|
|
154
|
+
panel: "计算器",
|
|
155
|
+
calculation: "算式",
|
|
156
|
+
backspace: "退格",
|
|
157
|
+
clear: "清除",
|
|
158
|
+
equals: "等于",
|
|
159
|
+
done: "完成",
|
|
160
|
+
plus: "加",
|
|
161
|
+
minus: "减",
|
|
162
|
+
times: "乘",
|
|
163
|
+
divide: "除以",
|
|
164
|
+
decimal: "小数点",
|
|
165
|
+
},
|
|
166
|
+
currency: {
|
|
167
|
+
currency: "货币",
|
|
168
|
+
search: "搜索货币",
|
|
169
|
+
},
|
|
170
|
+
chipInput: {
|
|
171
|
+
added: (value) => `已添加“${value}”`,
|
|
172
|
+
removed: (value) => `已移除“${value}”`,
|
|
173
|
+
remove: "移除",
|
|
174
|
+
atLimit: (max) => `已达上限(${n(max)} 项)`,
|
|
175
|
+
duplicate: (value) => `“${value}”已在列表中`,
|
|
176
|
+
},
|
|
177
|
+
swatchPicker: {
|
|
178
|
+
none: "无颜色",
|
|
179
|
+
mixed: "混合:所选项目的颜色各不相同",
|
|
180
|
+
},
|
|
181
|
+
iconPicker: {
|
|
182
|
+
none: "无图标",
|
|
183
|
+
mixed: "混合:所选项目的图标各不相同",
|
|
184
|
+
search: "搜索图标",
|
|
185
|
+
noResults: "没有匹配的图标",
|
|
186
|
+
resultCount: (count) => `${n(count)} 个图标`,
|
|
187
|
+
},
|
|
188
|
+
fieldSync: {
|
|
189
|
+
synced: "已保存",
|
|
190
|
+
edited: "有未保存的更改",
|
|
191
|
+
pending: "正在保存…",
|
|
192
|
+
error: "保存失败",
|
|
193
|
+
retry: "重试",
|
|
194
|
+
},
|
|
195
|
+
passwordReveal: {
|
|
196
|
+
show: "显示密码",
|
|
197
|
+
hide: "隐藏密码",
|
|
198
|
+
},
|
|
199
|
+
dangerConfirm: {
|
|
200
|
+
arm: "删除…",
|
|
201
|
+
confirm: "删除",
|
|
202
|
+
cancel: "取消",
|
|
203
|
+
prompt: "此操作无法撤销。",
|
|
204
|
+
password: "密码",
|
|
205
|
+
phrase: (phrase) => `输入“${phrase}”以确认`,
|
|
206
|
+
},
|
|
207
|
+
tabs: {
|
|
208
|
+
add: "添加标签页",
|
|
209
|
+
// Quoted, so the spacing rule holds whether the tab's name is Han or Latin.
|
|
210
|
+
remove: (tab) => `移除“${tab}”`,
|
|
211
|
+
},
|
|
212
|
+
appShell: {
|
|
213
|
+
collapse: "收起侧边栏",
|
|
214
|
+
expand: "展开侧边栏",
|
|
215
|
+
toggleGroup: (groupLabel) => `${groupLabel}:页面`,
|
|
216
|
+
},
|
|
217
|
+
topBar: {
|
|
218
|
+
theme: "切换主题",
|
|
219
|
+
palette: "外观预设",
|
|
220
|
+
language: "语言",
|
|
221
|
+
switchRole: "切换角色",
|
|
222
|
+
role: (value) => `角色:${value}`,
|
|
223
|
+
},
|
|
224
|
+
pickerSheet: {
|
|
225
|
+
close: "关闭",
|
|
226
|
+
},
|
|
227
|
+
dialogFrame: {
|
|
228
|
+
close: "关闭",
|
|
229
|
+
},
|
|
230
|
+
swipeableRow: {
|
|
231
|
+
actions: "行操作",
|
|
232
|
+
},
|
|
233
|
+
file: {
|
|
234
|
+
// The kit's own `Intl` unit formatting, pinned to this locale.
|
|
235
|
+
size: (bytes) => formatFileSize(bytes, numberLocale),
|
|
236
|
+
},
|
|
237
|
+
filePicker: {
|
|
238
|
+
rejectedPick: (count) => (count === 1 ? "未添加该文件" : `${count} 个文件均未添加`),
|
|
239
|
+
rejectedType: (name) => `“${name}”的文件类型不受支持`,
|
|
240
|
+
rejectedTypeOnly: (accept) => `仅限 ${accept} 文件`,
|
|
241
|
+
rejectedSize: (name, maxSize) => `“${name}”超过 ${maxSize}`,
|
|
242
|
+
rejectedCount: (name, maxFiles) => `未添加“${name}”:最多 ${n(maxFiles)} 个文件`,
|
|
243
|
+
rejectedInvalid: (name) => `“${name}”无法在此使用`,
|
|
244
|
+
rejectedMany: (count) => `${n(count)} 个文件未添加`,
|
|
245
|
+
selected: (count, firstName) =>
|
|
246
|
+
count === 1 ? `已选择“${firstName}”` : `已选择 ${n(count)} 个文件`,
|
|
247
|
+
remove: (name) => `移除“${name}”`,
|
|
248
|
+
clearAll: "移除所有文件",
|
|
249
|
+
removed: (name) => `已移除“${name}”`,
|
|
250
|
+
cleared: "已移除所有文件",
|
|
251
|
+
},
|
|
252
|
+
wizard: {
|
|
253
|
+
cancel: "取消",
|
|
254
|
+
back: "上一步",
|
|
255
|
+
next: "下一步",
|
|
256
|
+
skip: "跳过",
|
|
257
|
+
finish: "完成",
|
|
258
|
+
submitting: "正在创建…",
|
|
259
|
+
steps: "步骤",
|
|
260
|
+
step: (current, total) => `第 ${n(current)} 步,共 ${n(total)} 步`,
|
|
261
|
+
cancelTitle: "放弃此表单?",
|
|
262
|
+
confirmCancel: "已填写的内容将会丢失。",
|
|
263
|
+
cancelConfirmLabel: "放弃",
|
|
264
|
+
cancelDismissLabel: "继续编辑",
|
|
265
|
+
reviewTitle: "确认信息",
|
|
266
|
+
edit: "编辑",
|
|
267
|
+
missingRequired: "请填写所有必填字段。",
|
|
268
|
+
genericError: "发生错误",
|
|
269
|
+
},
|
|
270
|
+
tour: {
|
|
271
|
+
next: "下一步",
|
|
272
|
+
back: "上一步",
|
|
273
|
+
skip: "跳过",
|
|
274
|
+
done: "完成",
|
|
275
|
+
awaitClickHint: "点击高亮的元素以继续",
|
|
276
|
+
step: (current, total) => `${n(current)} / ${n(total)}`,
|
|
277
|
+
},
|
|
278
|
+
commandPalette: {
|
|
279
|
+
placeholder: "搜索…",
|
|
280
|
+
empty: "无结果",
|
|
281
|
+
loading: "搜索中…",
|
|
282
|
+
dialog: "搜索",
|
|
283
|
+
error: "搜索失败,请重试。",
|
|
284
|
+
},
|
|
285
|
+
sparkline: {
|
|
286
|
+
rising: (first, last) => `从 ${first} 上升到 ${last}`,
|
|
287
|
+
falling: (first, last) => `从 ${first} 下降到 ${last}`,
|
|
288
|
+
flat: (value) => `保持在 ${value} 不变`,
|
|
289
|
+
single: (value) => `仅一个值:${value}`,
|
|
290
|
+
noData: "无数据",
|
|
291
|
+
named: (name, summary) => `${name}:${summary}`,
|
|
292
|
+
},
|
|
293
|
+
statTile: {
|
|
294
|
+
increase: (amount) => `上升 ${amount}`,
|
|
295
|
+
decrease: (amount) => `下降 ${amount}`,
|
|
296
|
+
unchanged: "无变化",
|
|
297
|
+
better: (change) => `${change}(向好)`,
|
|
298
|
+
worse: (change) => `${change}(变差)`,
|
|
299
|
+
noValue: "无数据",
|
|
300
|
+
loading: "加载中…",
|
|
301
|
+
},
|
|
302
|
+
signaturePad: {
|
|
303
|
+
label: "签名",
|
|
304
|
+
instructions: "请使用鼠标、手指或触控笔在框内签名。",
|
|
305
|
+
typedFallbackHint: "如果无法手写,可以改为输入姓名。",
|
|
306
|
+
empty: "尚未签名",
|
|
307
|
+
signed: "已签名",
|
|
308
|
+
undo: "撤销上一笔",
|
|
309
|
+
clear: "清除",
|
|
310
|
+
save: "保存签名",
|
|
311
|
+
useTyped: "改为输入姓名",
|
|
312
|
+
useDrawn: "改为手写",
|
|
313
|
+
typedName: "姓名",
|
|
314
|
+
cleared: "签名已清除",
|
|
315
|
+
undone: "已撤销上一笔",
|
|
316
|
+
viewEmpty: "未签名",
|
|
317
|
+
viewDrawn: "手写签名",
|
|
318
|
+
// Quoted, so the spacing rule holds whether the name is Han or Latin.
|
|
319
|
+
viewTyped: (name) => `以输入的姓名“${name}”签名`,
|
|
320
|
+
},
|
|
321
|
+
passwordStrength: {
|
|
322
|
+
tooShort: "太短",
|
|
323
|
+
weak: "弱",
|
|
324
|
+
fair: "一般",
|
|
325
|
+
good: "良好",
|
|
326
|
+
strong: "强",
|
|
327
|
+
announcement: (level) => `密码强度:${level}`,
|
|
328
|
+
// Unformatted, as in the English default.
|
|
329
|
+
ruleLength: (minLength) => `至少 ${minLength} 个字符`,
|
|
330
|
+
ruleCase: "包含大写和小写字母",
|
|
331
|
+
ruleDigit: "包含数字",
|
|
332
|
+
ruleSymbol: "包含符号",
|
|
333
|
+
optional: (rule) => `${rule}(可选)`,
|
|
334
|
+
met: "已满足:",
|
|
335
|
+
notMet: "未满足:",
|
|
336
|
+
tooLong: (maxBytes) => `最多 ${maxBytes} 个字符(带重音的字母和表情符号按多个字符计算)。`,
|
|
337
|
+
},
|
|
338
|
+
seriesChart: {
|
|
339
|
+
resetZoom: "重置缩放",
|
|
340
|
+
zoomHint:
|
|
341
|
+
"拖动以缩放:近似正方形的选区同时缩放两条坐标轴,细长的选区只缩放其所沿的那条轴。双击可重置。",
|
|
342
|
+
empty: "无数据",
|
|
343
|
+
legend: "数据系列",
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export const UI_KIT_LABELS_ZH: UiKitLabels = uiKitLabelsZh();
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,9 @@ export * from "./components/swatch-picker";
|
|
|
87
87
|
export * from "./components/icon-picker";
|
|
88
88
|
export * from "./components/choice-card";
|
|
89
89
|
export * from "./components/autocomplete";
|
|
90
|
+
export * from "./components/measured-grid";
|
|
91
|
+
export { useWindowedRows } from "./hooks/use-windowed-rows";
|
|
92
|
+
export type { WindowedRows } from "./hooks/use-windowed-rows";
|
|
90
93
|
// Named, not `export *`: file-button.tsx also holds the screening helpers the
|
|
91
94
|
// dropzone shares, which are internal.
|
|
92
95
|
export { FileButton, useFilePicker, matchesAccept, DEFAULT_FILE_PICKER_LABELS } from "./components/file-button";
|
|
@@ -131,7 +134,13 @@ export { FilterPopover } from "./components/data-table-filter-popover";
|
|
|
131
134
|
// data-table re-exports SortState internally; export the rest explicitly to
|
|
132
135
|
// avoid a duplicate SortState star-export (it comes from data-table-sort).
|
|
133
136
|
export { DataTable } from "./components/data-table";
|
|
134
|
-
export type {
|
|
137
|
+
export type {
|
|
138
|
+
DataTableColumn,
|
|
139
|
+
DataTableProps,
|
|
140
|
+
ServerPagination,
|
|
141
|
+
FilterState,
|
|
142
|
+
MobileSwipeActions,
|
|
143
|
+
} from "./components/data-table";
|
|
135
144
|
|
|
136
145
|
// ── shell (composable app chrome) ────────────────────────────────────────────
|
|
137
146
|
export * from "./shell/topbar-controls";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading direction, as the DOM has it — internal, not exported from the barrel.
|
|
3
|
+
*
|
|
4
|
+
* The kit lays itself out with logical classes (`ps-*`, `end-*`, `text-start`), which
|
|
5
|
+
* need no JavaScript. These helpers are for the two things CSS cannot do:
|
|
6
|
+
*
|
|
7
|
+
* - **Arrow keys.** ←/→ mean previous/next in LTR and the reverse in RTL. Resolve the
|
|
8
|
+
* key through {@link horizontalStep} instead of comparing to "ArrowRight".
|
|
9
|
+
* - **Portals.** A panel portalled to `document.body` leaves the subtree whose `dir`
|
|
10
|
+
* it inherited. Read the anchor's direction with {@link dirOf} and put it on the
|
|
11
|
+
* portalled root, and position it from the anchor's START edge, not `rect.left`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Direction = "ltr" | "rtl";
|
|
15
|
+
|
|
16
|
+
/** The effective direction at `el`: its nearest `[dir]` ancestor (or itself), else the
|
|
17
|
+
* document's, else LTR. Attribute-based on purpose — jsdom computes no `direction`. */
|
|
18
|
+
export function dirOf(el: Element | null | undefined): Direction {
|
|
19
|
+
const source =
|
|
20
|
+
el?.closest("[dir]") ??
|
|
21
|
+
(typeof document !== "undefined" ? document.documentElement : null);
|
|
22
|
+
return source?.getAttribute("dir") === "rtl" ? "rtl" : "ltr";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isRtl(el: Element | null | undefined): boolean {
|
|
26
|
+
return dirOf(el) === "rtl";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* +1 for a key that moves FORWARD along the reading direction, -1 for backward, 0 for
|
|
31
|
+
* any other key. ArrowRight is forward in LTR and backward in RTL.
|
|
32
|
+
*/
|
|
33
|
+
export function horizontalStep(key: string, el: Element | null | undefined): 1 | -1 | 0 {
|
|
34
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
|
|
35
|
+
const forward = (key === "ArrowRight") !== isRtl(el);
|
|
36
|
+
return forward ? 1 : -1;
|
|
37
|
+
}
|
package/src/rhf/form.tsx
CHANGED
|
@@ -51,6 +51,27 @@
|
|
|
51
51
|
* `<Input {...field} invalid={fieldState.invalid} />`. `FormControl` does not inject
|
|
52
52
|
* `invalid` itself because its child may be a native element, which would print it
|
|
53
53
|
* into the DOM as an unknown attribute.
|
|
54
|
+
*
|
|
55
|
+
* ## Clearing a picker: `null`, unless the picker is told otherwise
|
|
56
|
+
*
|
|
57
|
+
* Nothing here touches a value. `FormField` is `Controller`, so what a control hands
|
|
58
|
+
* `field.onChange` is exactly what lands in the form — and what the entity pickers
|
|
59
|
+
* hand it on a clear is `null`: `EntityCombobox`'s "×", `InlineEntityCombobox`'s "×"
|
|
60
|
+
* and its emptied text. (`MultiEntityCombobox` and `MultiSelect` clear to `[]`.)
|
|
61
|
+
*
|
|
62
|
+
* A zod schema that spells "no choice" as `""` (`z.string()`, or `z.literal("")` in a
|
|
63
|
+
* union) then fails on the `null`. Say so on the PICKER, not here:
|
|
64
|
+
*
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <EntityCombobox clearValue="" value={field.value} onChange={field.onChange} … />
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* `clearValue` also types `onChange` (`(v: V | "") => void` rather than
|
|
70
|
+
* `V | null`) and reads `""` back as "nothing selected", so an initial `defaultValues`
|
|
71
|
+
* of `""` shows the placeholder rather than an unresolvable id. The adapter does not
|
|
72
|
+
* coerce because it cannot see the schema: mapping `null` to `""` for every field
|
|
73
|
+
* would break every schema that wants `null` (`z.string().nullable()`), which is the
|
|
74
|
+
* pickers' default for a reason — it is the one value no option can have.
|
|
54
75
|
*/
|
|
55
76
|
import {
|
|
56
77
|
Children,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { Search } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
7
|
+
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
8
|
+
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
|
|
7
9
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
8
10
|
|
|
9
11
|
export interface CommandItem {
|
|
@@ -42,6 +44,11 @@ export interface CommandPaletteLabels {
|
|
|
42
44
|
* `CommandPaletteLabels` keeps compiling; the default always carries it.
|
|
43
45
|
*/
|
|
44
46
|
dialog?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Shown in place of the results when the `search` provider throws or its promise
|
|
49
|
+
* rejects. Optional for the same reason as `dialog`; the default always carries it.
|
|
50
|
+
*/
|
|
51
|
+
error?: string;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {
|
|
@@ -49,6 +56,7 @@ export const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {
|
|
|
49
56
|
empty: "No results",
|
|
50
57
|
loading: "Searching…",
|
|
51
58
|
dialog: "Search",
|
|
59
|
+
error: "Search failed. Try again.",
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —
|
|
@@ -124,7 +132,16 @@ export function CommandPalette({
|
|
|
124
132
|
const [query, setQuery] = useState("");
|
|
125
133
|
const [results, setResults] = useState<CommandItem[]>([]);
|
|
126
134
|
const [loading, setLoading] = useState(false);
|
|
135
|
+
const [failed, setFailed] = useState(false);
|
|
127
136
|
const [active, setActive] = useState(0);
|
|
137
|
+
// Per instance: these were the literal "command-palette-list" / "command-item-<id>",
|
|
138
|
+
// so two palettes on one page (an app-wide one and a scoped one) pointed each
|
|
139
|
+
// other's `aria-controls` and `aria-activedescendant` at the wrong list.
|
|
140
|
+
const uid = useId();
|
|
141
|
+
const listId = `${uid}-list`;
|
|
142
|
+
const itemId = (id: string) => `${uid}-item-${id}`;
|
|
143
|
+
const groupId = (index: number) => `${uid}-group-${index}`;
|
|
144
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
128
145
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
129
146
|
const listRef = useRef<HTMLUListElement>(null);
|
|
130
147
|
const searchRef = useRef(search);
|
|
@@ -137,13 +154,17 @@ export function CommandPalette({
|
|
|
137
154
|
// mounting-while-open is what lets it sit here.
|
|
138
155
|
useOverlayHistory(open, onClose);
|
|
139
156
|
|
|
140
|
-
//
|
|
157
|
+
// A modal like any other: Tab stays inside, the page behind does not scroll, and
|
|
158
|
+
// focus goes back to whatever opened it — the palette used to drop it on <body>.
|
|
159
|
+
// Initial focus is the field, not the container: typing is the only reason to open it.
|
|
160
|
+
useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });
|
|
161
|
+
useBodyScrollLock(open);
|
|
162
|
+
|
|
163
|
+
// Reset when opened.
|
|
141
164
|
useEffect(() => {
|
|
142
165
|
if (!open) return;
|
|
143
166
|
setQuery("");
|
|
144
167
|
setActive(0);
|
|
145
|
-
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
146
|
-
return () => cancelAnimationFrame(id);
|
|
147
168
|
}, [open]);
|
|
148
169
|
|
|
149
170
|
// Debounced, race-safe search.
|
|
@@ -156,8 +177,18 @@ export function CommandPalette({
|
|
|
156
177
|
const r = await Promise.resolve(searchRef.current(query));
|
|
157
178
|
if (reqId.current === id) {
|
|
158
179
|
setResults(r);
|
|
180
|
+
setFailed(false);
|
|
181
|
+
setActive(0);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// There was no catch: the previous query's results stayed on screen as if they
|
|
185
|
+
// answered this one, and the rejection went unhandled. Clear them and say so.
|
|
186
|
+
if (reqId.current === id) {
|
|
187
|
+
setResults([]);
|
|
188
|
+
setFailed(true);
|
|
159
189
|
setActive(0);
|
|
160
190
|
}
|
|
191
|
+
console.error("CommandPalette: search failed", err);
|
|
161
192
|
} finally {
|
|
162
193
|
if (reqId.current === id) setLoading(false);
|
|
163
194
|
}
|
|
@@ -202,7 +233,13 @@ export function CommandPalette({
|
|
|
202
233
|
} else if (e.key === "Enter") {
|
|
203
234
|
e.preventDefault();
|
|
204
235
|
choose(flat[active]);
|
|
205
|
-
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Escape on the whole dialog, not only the field: once Tab has moved focus to a row
|
|
240
|
+
// (or a footer control), Escape used to do nothing at all.
|
|
241
|
+
const onDialogKeyDown = (e: React.KeyboardEvent) => {
|
|
242
|
+
if (e.key === "Escape") {
|
|
206
243
|
e.preventDefault();
|
|
207
244
|
onClose();
|
|
208
245
|
}
|
|
@@ -224,11 +261,17 @@ export function CommandPalette({
|
|
|
224
261
|
if (e.target === e.currentTarget) onClose();
|
|
225
262
|
}}
|
|
226
263
|
>
|
|
264
|
+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}
|
|
227
265
|
<div
|
|
266
|
+
ref={dialogRef}
|
|
228
267
|
role="dialog"
|
|
229
268
|
aria-modal="true"
|
|
230
269
|
aria-label={dialogName}
|
|
231
|
-
|
|
270
|
+
// The trap's fallback target when the field is not there to take focus.
|
|
271
|
+
tabIndex={-1}
|
|
272
|
+
// Escape bubbling up from anything inside — a dialog is where it belongs.
|
|
273
|
+
onKeyDown={onDialogKeyDown}
|
|
274
|
+
className="flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl outline-none"
|
|
232
275
|
>
|
|
233
276
|
<div className="flex items-center gap-2 border-b border-[var(--border)] px-3">
|
|
234
277
|
<Search className="size-4 shrink-0 text-[var(--text-placeholder)]" />
|
|
@@ -236,8 +279,8 @@ export function CommandPalette({
|
|
|
236
279
|
ref={inputRef}
|
|
237
280
|
role="combobox"
|
|
238
281
|
aria-expanded
|
|
239
|
-
aria-controls=
|
|
240
|
-
aria-activedescendant={flat[active] ?
|
|
282
|
+
aria-controls={listId}
|
|
283
|
+
aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}
|
|
241
284
|
value={query}
|
|
242
285
|
onChange={(e) => setQuery(e.target.value)}
|
|
243
286
|
onKeyDown={onKeyDown}
|
|
@@ -247,16 +290,31 @@ export function CommandPalette({
|
|
|
247
290
|
{loading && <span className="shrink-0 text-[11px] text-[var(--text-placeholder)]">{l.loading}</span>}
|
|
248
291
|
</div>
|
|
249
292
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
293
|
+
{/* The status lines sit outside the listbox: a listbox owns options and groups,
|
|
294
|
+
and "No results" is neither. */}
|
|
295
|
+
{failed && !loading && (
|
|
296
|
+
<div role="alert" className="px-3 py-6 text-center text-sm text-[var(--text-muted)]">
|
|
297
|
+
{l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
{!failed && flat.length === 0 && !loading && (
|
|
301
|
+
<div className="px-3 py-6 text-center text-sm text-[var(--text-muted)]">{l.empty}</div>
|
|
302
|
+
)}
|
|
303
|
+
{/* Listbox grouping: each group is a `role="group"` named by its heading, and
|
|
304
|
+
the list markup in between is presentational so the listbox's children are
|
|
305
|
+
groups and options only — as a plain <li>/<ul> tree a reader announced
|
|
306
|
+
"list, 2 items" between options and never tied the heading to its rows. */}
|
|
307
|
+
<ul id={listId} ref={listRef} role="listbox" aria-label={dialogName} className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
308
|
+
{groups.map(({ group, items }, groupIndex) => (
|
|
309
|
+
<li key={group} role="presentation">
|
|
310
|
+
<div
|
|
311
|
+
id={groupId(groupIndex)}
|
|
312
|
+
role="presentation"
|
|
313
|
+
className="px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]"
|
|
314
|
+
>
|
|
257
315
|
{group}
|
|
258
316
|
</div>
|
|
259
|
-
<ul>
|
|
317
|
+
<ul role="group" aria-labelledby={groupId(groupIndex)}>
|
|
260
318
|
{items.map((item) => {
|
|
261
319
|
flatIndex += 1;
|
|
262
320
|
const idx = flatIndex;
|
|
@@ -266,13 +324,13 @@ export function CommandPalette({
|
|
|
266
324
|
// `role="option"` on an <a href> is fine: the browser's middle-click
|
|
267
325
|
// behaviour keys off the element, not the ARIA role.
|
|
268
326
|
const shared = {
|
|
269
|
-
id:
|
|
327
|
+
id: itemId(item.id),
|
|
270
328
|
role: "option" as const,
|
|
271
329
|
"aria-selected": isActive,
|
|
272
330
|
"data-index": idx,
|
|
273
331
|
onMouseMove: () => setActive(idx),
|
|
274
332
|
className: cn(
|
|
275
|
-
"flex w-full items-center gap-2.5 px-3 py-2 text-
|
|
333
|
+
"flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm",
|
|
276
334
|
isActive
|
|
277
335
|
? "bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
278
336
|
: "text-[var(--text-secondary)]",
|
|
@@ -292,7 +350,7 @@ export function CommandPalette({
|
|
|
292
350
|
</>
|
|
293
351
|
);
|
|
294
352
|
return (
|
|
295
|
-
<li key={item.id}>
|
|
353
|
+
<li key={item.id} role="presentation">
|
|
296
354
|
{item.href ? (
|
|
297
355
|
<a
|
|
298
356
|
{...shared}
|