@eifi1/ui-kit 0.36.0 → 0.38.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 +11 -4
- package/dist/components/field-parts.d.ts +3 -3
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/help-popover.d.ts +1 -1
- package/dist/components/help-popover.js.map +1 -1
- package/dist/components/numeric-field.d.ts +1 -1
- package/dist/components/numeric-field.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +34 -2
- package/dist/eslint/role-utilities.js +30 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.d.ts +80 -0
- package/dist/feedback/feedback-comment-thread.js +162 -0
- package/dist/feedback/feedback-comment-thread.js.map +1 -0
- package/dist/feedback/feedback-dialog-labels.js +2 -1
- package/dist/feedback/feedback-dialog-labels.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +13 -0
- package/dist/feedback/feedback-dialog.js +12 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-menu-labels.js +2 -1
- package/dist/feedback/feedback-menu-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +17 -3
- package/dist/feedback/feedback-menu.js +11 -1
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-public-labels.d.ts +50 -0
- package/dist/feedback/feedback-public-labels.js +26 -0
- package/dist/feedback/feedback-public-labels.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +118 -0
- package/dist/feedback/feedback-public-list.js +248 -0
- package/dist/feedback/feedback-public-list.js.map +1 -0
- package/dist/feedback/feedback-public-switch.d.ts +51 -0
- package/dist/feedback/feedback-public-switch.js +50 -0
- package/dist/feedback/feedback-public-switch.js.map +1 -0
- package/dist/feedback/feedback-public-types.d.ts +75 -0
- package/dist/feedback/feedback-public-types.js +2 -0
- package/dist/feedback/feedback-public-types.js.map +1 -0
- package/dist/feedback/feedback-record.d.ts +32 -1
- package/dist/feedback/feedback-record.js +10 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +97 -0
- package/dist/feedback/feedback-replies.js +114 -0
- package/dist/feedback/feedback-replies.js.map +1 -0
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +38 -10
- package/dist/feedback/feedback-row-detail.js +50 -50
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +3 -1
- package/dist/feedback/feedback-submit-dialog.js +6 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +8 -0
- package/dist/feedback/feedback-submit.js +3 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +15 -5
- package/dist/feedback/feedback-table.js +46 -5
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-parts-labels.d.ts +6 -0
- package/dist/feedback/feedback-thread-parts-labels.js +36 -0
- package/dist/feedback/feedback-thread-parts-labels.js.map +1 -0
- package/dist/feedback/feedback-thread.d.ts +8 -0
- package/dist/feedback/feedback-thread.js +7 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vote-button.d.ts +48 -0
- package/dist/feedback/feedback-vote-button.js +72 -0
- package/dist/feedback/feedback-vote-button.js.map +1 -0
- package/dist/feedback.d.ts +6 -0
- package/dist/feedback.js +6 -0
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/defaults.js +12 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +64 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +20 -3
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +80 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +64 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +64 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +64 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +64 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +64 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +4 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/plain-text.d.ts +1 -1
- package/dist/lib/plain-text.js.map +1 -1
- package/dist/notifications/feedback-notification-types-labels.d.ts +15 -0
- package/dist/notifications/feedback-notification-types-labels.js +11 -0
- package/dist/notifications/feedback-notification-types-labels.js.map +1 -0
- package/dist/notifications/feedback-notification-types.d.ts +28 -0
- package/dist/notifications/feedback-notification-types.js +21 -0
- package/dist/notifications/feedback-notification-types.js.map +1 -0
- package/dist/rhf/fields.d.ts +1 -1
- package/dist/rhf/fields.js.map +1 -1
- package/dist/settings/device-reset-setting.d.ts +4 -4
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/package.json +1 -1
- package/src/components/field-parts.tsx +4 -4
- package/src/components/help-popover.tsx +1 -1
- package/src/components/numeric-field.ts +1 -1
- package/src/eslint/role-utilities.ts +67 -11
- package/src/feedback/feedback-comment-thread.tsx +255 -0
- package/src/feedback/feedback-dialog-labels.ts +1 -0
- package/src/feedback/feedback-dialog.tsx +30 -2
- package/src/feedback/feedback-menu-labels.ts +1 -0
- package/src/feedback/feedback-menu.tsx +29 -3
- package/src/feedback/feedback-public-labels.ts +76 -0
- package/src/feedback/feedback-public-list.tsx +395 -0
- package/src/feedback/feedback-public-switch.tsx +102 -0
- package/src/feedback/feedback-public-types.ts +82 -0
- package/src/feedback/feedback-record.ts +39 -1
- package/src/feedback/feedback-replies.tsx +235 -0
- package/src/feedback/feedback-row-detail-labels.ts +1 -0
- package/src/feedback/feedback-row-detail.tsx +103 -74
- package/src/feedback/feedback-submit-dialog.tsx +9 -0
- package/src/feedback/feedback-submit.tsx +10 -1
- package/src/feedback/feedback-table.tsx +73 -6
- package/src/feedback/feedback-thread-parts-labels.ts +41 -0
- package/src/feedback/feedback-thread.tsx +18 -1
- package/src/feedback/feedback-vote-button.tsx +122 -0
- package/src/feedback.ts +10 -0
- package/src/i18n/defaults.ts +12 -0
- package/src/i18n/german.ts +62 -0
- package/src/i18n/kit-labels.tsx +20 -3
- package/src/i18n/locales/en.ts +7 -0
- package/src/i18n/locales/es.ts +62 -0
- package/src/i18n/locales/fr.ts +62 -0
- package/src/i18n/locales/hu.ts +62 -0
- package/src/i18n/locales/it.ts +62 -0
- package/src/i18n/locales/zh.ts +62 -0
- package/src/i18n/review.ts +4 -0
- package/src/i18n/shared-labels.ts +1 -1
- package/src/index.ts +9 -0
- package/src/lib/plain-text.ts +1 -1
- package/src/notifications/feedback-notification-types-labels.ts +24 -0
- package/src/notifications/feedback-notification-types.ts +46 -0
- package/src/rhf/fields.tsx +1 -1
- package/src/settings/device-reset-setting.tsx +4 -4
|
@@ -50,9 +50,9 @@ export interface DeviceResetSettingProps extends Omit<SettingsSectionProps, "anc
|
|
|
50
50
|
* - **Live, no reload** (decision 10): each registered store resets through its own
|
|
51
51
|
* setter; the card stays where it is.
|
|
52
52
|
*
|
|
53
|
-
* The app adds the catalogue entry, `{ id
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* development without it.
|
|
53
|
+
* The app adds the catalogue entry, `{ id, group: "appearance", anchor: DEVICE_RESET_ANCHOR,
|
|
54
|
+
* title, keywords }` (the id in its catalogue's own style, e.g. `device_reset` or
|
|
55
|
+
* `deviceReset`; `title` and `keywords` from these labels), last in its appearance group:
|
|
56
|
+
* inside a `SettingsLayout`, `SettingsSection` throws in development without it.
|
|
57
57
|
*/
|
|
58
58
|
export declare function DeviceResetSetting({ description, labels: labelsProp, onReset, ...rest }: DeviceResetSettingProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/settings/device-reset-setting.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { resetDeviceSettings } from \"../lib/device-settings\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport { toast } from \"../components/toast\";\nimport { SettingsSection, type SettingsSectionProps } from \"./settings-section\";\nimport { DEFAULT_DEVICE_RESET_LABELS } from \"./device-reset-setting-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`\n * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */\nexport interface DeviceResetLabels {\n /** The card's title, and the app's catalogue entry's. */\n title: string;\n /** The line under it: what goes back, and that the account is not touched. */\n description: string;\n /** The button that asks first. */\n action: string;\n /** The question it asks. */\n confirmTitle: string;\n /** What goes, what follows the account again, and what stays. */\n confirmBody: string;\n /** The button that answers it. */\n confirmAction: string;\n /** The toast once it is done. */\n done: string;\n /** For the app's catalogue entry (`keywords`); the card never reads it. */\n keywords: string;\n}\n\n// Defined in ./device-reset-setting-labels, which holds no component code.\nexport { DEFAULT_DEVICE_RESET_LABELS };\n\n/* ── The card ────────────────────────────────────────────────────────────── */\n\n/** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */\nexport const DEVICE_RESET_ANCHOR = \"settings-device-reset\";\n\n// `onReset` is the card's own: the DOM's form-reset handler never fires on a div.\nexport interface DeviceResetSettingProps\n extends Omit<SettingsSectionProps, \"anchor\" | \"title\" | \"description\" | \"children\" | \"onReset\"> {\n /** The app's sentence after the kit's description: what of its own goes back, and what\n * stays (keksdose: the date format, the feature set and the gestures go; presentation\n * mode and the home position stay). */\n description?: ReactNode;\n labels?: LabelOverride<DeviceResetLabels>;\n /** Runs after the reset and before the toast, with the keys reset or removed. */\n onReset?: (keys: string[]) => void;\n}\n\n/**\n * \"Reset this device's settings\" (docs/settings-harmonization.md §11): the LAST card of\n * the appearance group. It puts this device's appearance — theme, palette, language, text\n * size, contrast and the app's own appearance choices — and its views — table views, the\n * sidebar, folded groups, saved filters — back to what a new device gets, through\n * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting\n * to be sent are never touched: they never join the registry.\n *\n * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,\n * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.\n * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's\n * write lock does not disable it (decision 3).\n * - **Live, no reload** (decision 10): each registered store resets through its own\n * setter; the card stays where it is.\n *\n * The app adds the catalogue entry, `{ id
|
|
1
|
+
{"version":3,"sources":["../../src/settings/device-reset-setting.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { resetDeviceSettings } from \"../lib/device-settings\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport { toast } from \"../components/toast\";\nimport { SettingsSection, type SettingsSectionProps } from \"./settings-section\";\nimport { DEFAULT_DEVICE_RESET_LABELS } from \"./device-reset-setting-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string {@link DeviceResetSetting} renders — the `deviceReset` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`\n * (docs/settings-harmonization.md §11.3). The confirm's Cancel is `confirmDialog.cancel`. */\nexport interface DeviceResetLabels {\n /** The card's title, and the app's catalogue entry's. */\n title: string;\n /** The line under it: what goes back, and that the account is not touched. */\n description: string;\n /** The button that asks first. */\n action: string;\n /** The question it asks. */\n confirmTitle: string;\n /** What goes, what follows the account again, and what stays. */\n confirmBody: string;\n /** The button that answers it. */\n confirmAction: string;\n /** The toast once it is done. */\n done: string;\n /** For the app's catalogue entry (`keywords`); the card never reads it. */\n keywords: string;\n}\n\n// Defined in ./device-reset-setting-labels, which holds no component code.\nexport { DEFAULT_DEVICE_RESET_LABELS };\n\n/* ── The card ────────────────────────────────────────────────────────────── */\n\n/** The card's DOM id: its catalogue entry's `anchor`, the `?focus=` target. */\nexport const DEVICE_RESET_ANCHOR = \"settings-device-reset\";\n\n// `onReset` is the card's own: the DOM's form-reset handler never fires on a div.\nexport interface DeviceResetSettingProps\n extends Omit<SettingsSectionProps, \"anchor\" | \"title\" | \"description\" | \"children\" | \"onReset\"> {\n /** The app's sentence after the kit's description: what of its own goes back, and what\n * stays (keksdose: the date format, the feature set and the gestures go; presentation\n * mode and the home position stay). */\n description?: ReactNode;\n labels?: LabelOverride<DeviceResetLabels>;\n /** Runs after the reset and before the toast, with the keys reset or removed. */\n onReset?: (keys: string[]) => void;\n}\n\n/**\n * \"Reset this device's settings\" (docs/settings-harmonization.md §11): the LAST card of\n * the appearance group. It puts this device's appearance — theme, palette, language, text\n * size, contrast and the app's own appearance choices — and its views — table views, the\n * sidebar, folded groups, saved filters — back to what a new device gets, through\n * {@link resetDeviceSettings}. The account, the sign-in, the session and anything waiting\n * to be sent are never touched: they never join the registry.\n *\n * - **A confirm, not an undo** (§11.2 decision 9): `useConfirm` in the neutral tone,\n * then a toast. An app mounts a `ConfirmProvider` and a `Toaster` already.\n * - **Not a commit**: nothing is written to the account, so a demo or a lapsed plan's\n * write lock does not disable it (decision 3).\n * - **Live, no reload** (decision 10): each registered store resets through its own\n * setter; the card stays where it is.\n *\n * The app adds the catalogue entry, `{ id, group: \"appearance\", anchor: DEVICE_RESET_ANCHOR,\n * title, keywords }` (the id in its catalogue's own style, e.g. `device_reset` or\n * `deviceReset`; `title` and `keywords` from these labels), last in its appearance group:\n * inside a `SettingsLayout`, `SettingsSection` throws in development without it.\n */\nexport function DeviceResetSetting({\n description,\n labels: labelsProp,\n onReset,\n ...rest\n}: DeviceResetSettingProps) {\n const labels = useKitLabels(\"deviceReset\", DEFAULT_DEVICE_RESET_LABELS, labelsProp);\n const confirm = useConfirm();\n\n const reset = async () => {\n const ok = await confirm({\n title: labels.confirmTitle,\n body: labels.confirmBody,\n confirmLabel: labels.confirmAction,\n tone: \"neutral\",\n });\n if (!ok) return;\n const keys = resetDeviceSettings();\n onReset?.(keys);\n toast.success(labels.done);\n };\n\n return (\n <SettingsSection\n {...rest}\n anchor={DEVICE_RESET_ANCHOR}\n title={labels.title}\n description={\n description != null ? (\n <>\n {labels.description} {description}\n </>\n ) : (\n labels.description\n )\n }\n >\n <div>\n <Button variant=\"secondary\" onClick={() => void reset()}>\n {labels.action}\n </Button>\n </div>\n </SettingsSection>\n );\n}\n"],"mappings":";AAuGU,mBASF,KATE;AArGV,SAAS,oBAAwC;AACjD,SAAS,2BAA2B;AACpC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,uBAAkD;AAC3D,SAAS,mCAAmC;AAgCrC,MAAM,sBAAsB;AAkC5B,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA4B;AAC1B,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU;AAClF,QAAM,UAAU,WAAW;AAE3B,QAAM,QAAQ,YAAY;AACxB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,oBAAoB;AACjC,cAAU,IAAI;AACd,UAAM,QAAQ,OAAO,IAAI;AAAA,EAC3B;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,QAAQ;AAAA,MACR,OAAO,OAAO;AAAA,MACd,aACE,eAAe,OACb,iCACG;AAAA,eAAO;AAAA,QAAY;AAAA,QAAE;AAAA,SACxB,IAEA,OAAO;AAAA,MAIX,8BAAC,SACC,8BAAC,UAAO,SAAQ,aAAY,SAAS,MAAM,KAAK,MAAM,GACnD,iBAAO,QACV,GACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|
|
@@ -232,7 +232,7 @@ export function useInlineHint(hint: ReactNode, withHelp = false): ReactNode {
|
|
|
232
232
|
*
|
|
233
233
|
* - `hint` is the fact, the caption under the field, on its `aria-describedby`;
|
|
234
234
|
* - `help` is the help, a {@link HelpPopover} "?" on the label line (an unlabelled
|
|
235
|
-
* field's at its end edge, as a hint's), named "About
|
|
235
|
+
* field's at its end edge, as a hint's), named "About “<label>”" (`common.aboutField`,
|
|
236
236
|
* from the label's text, else the `aria-label`, else `common.help`). Pass a
|
|
237
237
|
* `HelpPopover` as `help` to name it yourself ("What do the statuses mean?").
|
|
238
238
|
*
|
|
@@ -245,7 +245,7 @@ export interface FieldHelpProps {
|
|
|
245
245
|
/**
|
|
246
246
|
* Help the field is usable without — why it exists, how to choose, what the options
|
|
247
247
|
* mean — behind the field's one "?" on its label line: a {@link HelpPopover} named
|
|
248
|
-
* "About
|
|
248
|
+
* "About “<label>”" that opens on a tap (round 0.36, docs/text-size-harmonization.md §4).
|
|
249
249
|
* Pass a `HelpPopover` to name it yourself. `hint` stays the visible fact: with `help`
|
|
250
250
|
* set it is the caption under the field at every size, a `FieldHint` included.
|
|
251
251
|
*/
|
|
@@ -253,7 +253,7 @@ export interface FieldHelpProps {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
|
-
* The "?" for a field's `help` — the field's own name, "About
|
|
256
|
+
* The "?" for a field's `help` — the field's own name, "About “<label>”", in the reader's
|
|
257
257
|
* language. A component so that a field without `help` asks the labels nothing.
|
|
258
258
|
*/
|
|
259
259
|
function FieldHelp({ name, children }: { name: ReactNode; children: ReactNode }) {
|
|
@@ -265,7 +265,7 @@ function FieldHelp({ name, children }: { name: ReactNode; children: ReactNode })
|
|
|
265
265
|
/**
|
|
266
266
|
* A field's `help` as the "?" for its label line, or `undefined` when there is none. A
|
|
267
267
|
* {@link HelpPopover} is used as it is (it carries its own name); anything else is put
|
|
268
|
-
* in one named "About
|
|
268
|
+
* in one named "About “<name>”" — `name` is the field's label, else its `aria-label`.
|
|
269
269
|
*/
|
|
270
270
|
export function fieldHelpMark(help: ReactNode, name: ReactNode): ReactNode | undefined {
|
|
271
271
|
if (!hasContent(help)) return undefined;
|
|
@@ -45,7 +45,7 @@ const HELP_WIDTH: RemLength = "20rem";
|
|
|
45
45
|
* **On a field: its `help`** (0.36, round 0.36 decision 1). Every kit field that takes a
|
|
46
46
|
* `hint` takes a `help` beside it, and the `@eifi1/ui-kit/rhf` bindings too: pass the
|
|
47
47
|
* help's words and the field draws this "?" on its label line (an unlabelled one at its
|
|
48
|
-
* end edge, outside the box), named "About
|
|
48
|
+
* end edge, outside the box), named "About “<label>”" — or pass a HelpPopover to name it
|
|
49
49
|
* yourself. `hint` stays the FACT, printed under the field; while `help` is set a
|
|
50
50
|
* FieldHint there draws no "?" of its own, so the field has one. Passed as `hint`, as in
|
|
51
51
|
* 0.34, it still rides the label line. It also stands on its own, beside a heading or a
|
|
@@ -30,7 +30,7 @@ import type { CommitScope } from "./write-lock";
|
|
|
30
30
|
|
|
31
31
|
/** The props both fields take and this engine reads. */
|
|
32
32
|
export interface NumericFieldProps extends FieldHelpProps {
|
|
33
|
-
/** What names the `help` "?" — "About
|
|
33
|
+
/** What names the `help` "?" — "About “<label>”" — with the `aria-label`. */
|
|
34
34
|
label?: ReactNode;
|
|
35
35
|
hint?: ReactNode;
|
|
36
36
|
error?: ReactNode;
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// The table itself is role-utilities.generated.ts, re-rendered from tokens.css's
|
|
6
6
|
// `@theme inline` block by scripts/gen-role-utilities.mjs with the parser below; the
|
|
7
7
|
// check fails while it is stale. So tokens.css stays the source: a role that gains a
|
|
8
|
-
// utility there is flagged in the kit and in the apps from the next release on.
|
|
8
|
+
// utility there is flagged in the kit and in the apps from the next release on. The
|
|
9
|
+
// README's role-utility table (scripts/gen-styling-inventory.mjs) reads the block with
|
|
10
|
+
// the same parser since 0.37, so the README, the check and the plugin cannot disagree.
|
|
9
11
|
//
|
|
10
12
|
// This module imports NOTHING, and its TypeScript is erasable only (no enum, no
|
|
11
13
|
// namespace, no parameter property): the check imports it from source under Node's type
|
|
@@ -43,20 +45,74 @@ export type RoleUtilityTable = {
|
|
|
43
45
|
readonly [P in RoleProperty]: Readonly<Record<string, readonly string[]>>;
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
/** One `--<namespace>-<name>: <value>;` of the `@theme inline` block. */
|
|
49
|
+
export interface ThemeInlineDeclaration {
|
|
50
|
+
/** The theme variable without its dashes: `text-color-muted`, `ring-width-focus`. */
|
|
51
|
+
variable: string;
|
|
52
|
+
/** The property whose namespace it is in; `null` for a namespace ROLE_NAMESPACES lacks. */
|
|
53
|
+
property: RoleProperty | null;
|
|
54
|
+
/** The name after the namespace, `muted`; `null` with `property`. */
|
|
55
|
+
name: string | null;
|
|
56
|
+
/** The utility it makes: `text-muted`, `ring-focus` (a width is `ring-*` like a colour). */
|
|
57
|
+
utility: string | null;
|
|
58
|
+
/** The token the value reads, `--text-muted`; `null` unless the value is a bare `var()`. */
|
|
59
|
+
token: string | null;
|
|
60
|
+
/** The value as written, trimmed. */
|
|
61
|
+
value: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The `@theme inline` block: where it stands, and every declaration in it in block order. */
|
|
65
|
+
export interface ThemeInlineBlock {
|
|
66
|
+
/** Its first and one-past-last offset in `stripCssComments(css)`, not in `css`. */
|
|
67
|
+
start: number;
|
|
68
|
+
end: number;
|
|
69
|
+
declarations: ThemeInlineDeclaration[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `css` without its comments: a declaration commented out is not one. */
|
|
73
|
+
export function stripCssComments(css: string): string {
|
|
74
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const PROPERTIES = Object.keys(ROLE_NAMESPACES) as RoleProperty[];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* tokens.css's `@theme inline` block, read once for every reader: the table below, and
|
|
81
|
+
* the README's (scripts/gen-styling-inventory.mjs), which fails on a `property: null`
|
|
82
|
+
* where the table leaves it out. `null` when there is no such block — the role utilities
|
|
83
|
+
* are gone, or this parser is. Theme blocks hold only declarations, so the first `}`
|
|
84
|
+
* closes it; no namespace is another's prefix, so the first that fits is the one.
|
|
85
|
+
*/
|
|
86
|
+
export function parseThemeInline(css: string): ThemeInlineBlock | null {
|
|
87
|
+
const block = /@theme\s+inline\s*\{([^}]*)\}/.exec(stripCssComments(css));
|
|
88
|
+
if (!block) return null;
|
|
89
|
+
const declarations = [...block[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)].map(([, variable, raw]) => {
|
|
90
|
+
const value = raw.trim();
|
|
91
|
+
const property = PROPERTIES.find((p) => variable.startsWith(`${ROLE_NAMESPACES[p]}-`)) ?? null;
|
|
92
|
+
const name = property && variable.slice(ROLE_NAMESPACES[property].length + 1);
|
|
93
|
+
return {
|
|
94
|
+
variable,
|
|
95
|
+
property,
|
|
96
|
+
name,
|
|
97
|
+
utility: property && `${property.replace(/-width$/, "")}-${name}`,
|
|
98
|
+
token: /^var\((--[a-z0-9-]+)\)$/.exec(value)?.[1] ?? null,
|
|
99
|
+
value,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
return { start: block.index, end: block.index + block[0].length, declarations };
|
|
103
|
+
}
|
|
104
|
+
|
|
46
105
|
/**
|
|
47
106
|
* The table `tokens.css` declares: every `--<namespace>-<name>: var(--<token>)` in its
|
|
48
|
-
* `@theme inline` block, in block order
|
|
49
|
-
*
|
|
107
|
+
* `@theme inline` block, in block order, each property present (in ROLE_NAMESPACES' order)
|
|
108
|
+
* even when it has none. `null` when there is no such block.
|
|
50
109
|
*/
|
|
51
110
|
export function roleUtilitiesFromCss(css: string): RoleUtilityTable | null {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
const table = {} as Record<RoleProperty, Record<string, string[]>>;
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
const declared = new RegExp(`--${ROLE_NAMESPACES[prop]}-([a-z0-9-]+)\\s*:\\s*var\\((--[a-z0-9-]+)\\)`, "g");
|
|
58
|
-
for (const [, name, token] of theme.matchAll(declared)) (names[token] ??= []).push(name);
|
|
59
|
-
table[prop] = names;
|
|
111
|
+
const block = parseThemeInline(css);
|
|
112
|
+
if (!block) return null;
|
|
113
|
+
const table = Object.fromEntries(PROPERTIES.map((p) => [p, {}])) as Record<RoleProperty, Record<string, string[]>>;
|
|
114
|
+
for (const { property, name, token } of block.declarations) {
|
|
115
|
+
if (property && name && token) (table[property][token] ??= []).push(name);
|
|
60
116
|
}
|
|
61
117
|
return table;
|
|
62
118
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { EyeOff } from "lucide-react";
|
|
3
|
+
import { Button } from "../components/button";
|
|
4
|
+
import { useConfirm } from "../components/confirm-dialog";
|
|
5
|
+
import type { CommitScope } from "../components/write-lock";
|
|
6
|
+
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
7
|
+
import { FeedbackStatusBadge } from "./feedback-inbox";
|
|
8
|
+
import type { FeedbackComment, FeedbackReply } from "./feedback-public-types";
|
|
9
|
+
import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
|
|
10
|
+
import { FEEDBACK_COMMENT_MAX } from "./feedback-record";
|
|
11
|
+
import { useFeedbackRepliesLabels } from "./feedback-replies";
|
|
12
|
+
import { ChatComposer, FeedbackThread } from "./feedback-thread";
|
|
13
|
+
import type { FeedbackThreadMessage } from "./feedback-thread";
|
|
14
|
+
import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A report's conversation as everyone who may read it sees it
|
|
18
|
+
* (docs/feedback-harmonization.md §9.8): the admin's replies and other people's
|
|
19
|
+
* comments, each its own row, oldest first — merged by time, on the
|
|
20
|
+
* {@link FeedbackThread} look — and the box to add one.
|
|
21
|
+
*
|
|
22
|
+
* - **Replies** are drawn as staff posts with the status they set ("→ Done"); a reply
|
|
23
|
+
* without an author is "Admin" (§9.7).
|
|
24
|
+
* - **Comments**: the viewer's own at the reading end ("You"); someone else's under the
|
|
25
|
+
* first name the server sent, `null` as "A user" (§9.5, `feedbackPublic.anonymous`).
|
|
26
|
+
* - **Hidden** (§9.8): others see "Hidden by an admin" and no name, no text; the writer
|
|
27
|
+
* sees their text with that line; the admin sees it struck through, with **Unhide**.
|
|
28
|
+
* An admin has **Hide** on every visible comment; the writer **Delete** on their own,
|
|
29
|
+
* after a confirm (it is a hard delete). Nobody edits a comment.
|
|
30
|
+
* - **The composer** (1–4 000 characters, no files) only with `onComment`: left out for
|
|
31
|
+
* a reader who may not write (a demo, a CRASH report — the app knows).
|
|
32
|
+
*
|
|
33
|
+
* Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the
|
|
34
|
+
* viewer's needs the app's `ConfirmProvider` (each app mounts one already).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/* ── Labels ───────────────────────────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
/** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is
|
|
40
|
+
* `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */
|
|
41
|
+
export interface FeedbackCommentsLabels {
|
|
42
|
+
/** The thread's accessible name. de-CH: "Kommentare" */
|
|
43
|
+
comments: string;
|
|
44
|
+
/** Nothing yet. de-CH: "Noch keine Kommentare" */
|
|
45
|
+
empty: string;
|
|
46
|
+
/** In place of a hidden comment. de-CH: "Von einem Admin ausgeblendet" */
|
|
47
|
+
hidden: string;
|
|
48
|
+
/** The admin's button. de-CH: "Ausblenden" */
|
|
49
|
+
hide: string;
|
|
50
|
+
/** The admin's button on a hidden comment. de-CH: "Einblenden" */
|
|
51
|
+
unhide: string;
|
|
52
|
+
/** The writer's button. de-CH: "Löschen" */
|
|
53
|
+
delete: string;
|
|
54
|
+
/** The confirm's question. de-CH: "Diesen Kommentar löschen?" */
|
|
55
|
+
deleteTitle: string;
|
|
56
|
+
/** The confirm's line. de-CH: "Er wird für alle entfernt. Das kann nicht rückgängig
|
|
57
|
+
* gemacht werden." */
|
|
58
|
+
deleteBody: string;
|
|
59
|
+
/** The box's accessible name. de-CH: "Kommentar schreiben" */
|
|
60
|
+
field: string;
|
|
61
|
+
/** de-CH: "Kommentar schreiben…" */
|
|
62
|
+
placeholder: string;
|
|
63
|
+
/** The send button. de-CH: "Kommentieren" */
|
|
64
|
+
send: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { DEFAULT_FEEDBACK_COMMENTS_LABELS };
|
|
68
|
+
|
|
69
|
+
/* ── The thread ───────────────────────────────────────────────────────────── */
|
|
70
|
+
|
|
71
|
+
export interface FeedbackCommentThreadProps {
|
|
72
|
+
/** The report's replies (§9.7), oldest first. `[]` when it has none. */
|
|
73
|
+
replies: readonly FeedbackReply[];
|
|
74
|
+
/** `GET /feedback/{id}/comments`, oldest first. */
|
|
75
|
+
comments: readonly FeedbackComment[];
|
|
76
|
+
/** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */
|
|
77
|
+
isAdmin: boolean;
|
|
78
|
+
/** `POST /feedback/{id}/comments`. Return the promise: the box clears when it
|
|
79
|
+
* resolves. Left out, no composer. */
|
|
80
|
+
onComment?: (body: string) => void | Promise<unknown>;
|
|
81
|
+
/** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. Left out, no
|
|
82
|
+
* Hide / Unhide. */
|
|
83
|
+
onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;
|
|
84
|
+
/** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the
|
|
85
|
+
* confirm. Left out, no Delete. */
|
|
86
|
+
onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;
|
|
87
|
+
/** A comment is being sent. */
|
|
88
|
+
pending?: boolean;
|
|
89
|
+
/** The comments are loading. */
|
|
90
|
+
loading?: boolean;
|
|
91
|
+
/** Comment, Hide and Delete COMMIT — see Button's `commit`. */
|
|
92
|
+
commit?: CommitScope;
|
|
93
|
+
/** "Now" for the relative times — pin it in tests and stories. */
|
|
94
|
+
now?: Date | number;
|
|
95
|
+
labels?: LabelOverride<FeedbackCommentsLabels>;
|
|
96
|
+
className?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type Entry = { at: number; order: number; message: FeedbackThreadMessage };
|
|
100
|
+
|
|
101
|
+
const instant = (iso: string) => {
|
|
102
|
+
const ms = Date.parse(iso);
|
|
103
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function FeedbackCommentThread({
|
|
107
|
+
replies,
|
|
108
|
+
comments,
|
|
109
|
+
isAdmin,
|
|
110
|
+
onComment,
|
|
111
|
+
onHide,
|
|
112
|
+
onDelete,
|
|
113
|
+
pending = false,
|
|
114
|
+
loading = false,
|
|
115
|
+
commit,
|
|
116
|
+
now,
|
|
117
|
+
labels: labelsProp,
|
|
118
|
+
className,
|
|
119
|
+
}: FeedbackCommentThreadProps) {
|
|
120
|
+
const labels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);
|
|
121
|
+
const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
|
|
122
|
+
const replyLabels = useFeedbackRepliesLabels();
|
|
123
|
+
|
|
124
|
+
const entries: Entry[] = [];
|
|
125
|
+
replies.forEach((reply, order) => {
|
|
126
|
+
entries.push({
|
|
127
|
+
at: instant(reply.created_at),
|
|
128
|
+
order,
|
|
129
|
+
message: {
|
|
130
|
+
id: `reply-${reply.id}`,
|
|
131
|
+
author: reply.author_name ?? replyLabels.admin,
|
|
132
|
+
createdAt: reply.created_at,
|
|
133
|
+
body: reply.body,
|
|
134
|
+
staff: true,
|
|
135
|
+
badge: reply.status_to ? (
|
|
136
|
+
<span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
|
|
137
|
+
<span className="sr-only">{replyLabels.statusChanged}</span>
|
|
138
|
+
<span aria-hidden>→</span>
|
|
139
|
+
<FeedbackStatusBadge status={reply.status_to} />
|
|
140
|
+
</span>
|
|
141
|
+
) : undefined,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
comments.forEach((comment, index) => {
|
|
146
|
+
// Shown as hidden — no name, no text — to anyone but its writer and an admin.
|
|
147
|
+
const veiled = comment.hidden && !comment.mine && !isAdmin;
|
|
148
|
+
const actions: ReactNode[] = [];
|
|
149
|
+
if (comment.hidden) {
|
|
150
|
+
actions.push(
|
|
151
|
+
<span key="hidden" className="inline-flex items-center gap-1" data-feedback-comment-hidden="">
|
|
152
|
+
<EyeOff className="size-3" aria-hidden />
|
|
153
|
+
{labels.hidden}
|
|
154
|
+
</span>,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (isAdmin && onHide) {
|
|
158
|
+
actions.push(
|
|
159
|
+
<Button
|
|
160
|
+
key="hide"
|
|
161
|
+
type="button"
|
|
162
|
+
variant="link"
|
|
163
|
+
tone="muted"
|
|
164
|
+
size="sm"
|
|
165
|
+
commit={commit}
|
|
166
|
+
onClick={() => void onHide(comment, !comment.hidden)}
|
|
167
|
+
>
|
|
168
|
+
{comment.hidden ? labels.unhide : labels.hide}
|
|
169
|
+
</Button>,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (comment.mine && onDelete) {
|
|
173
|
+
actions.push(
|
|
174
|
+
<DeleteCommentButton key="delete" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
entries.push({
|
|
178
|
+
at: instant(comment.created_at),
|
|
179
|
+
// Replies first on a tie, then the comments in their order.
|
|
180
|
+
order: replies.length + index,
|
|
181
|
+
message: {
|
|
182
|
+
id: `comment-${comment.id}`,
|
|
183
|
+
own: comment.mine,
|
|
184
|
+
author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),
|
|
185
|
+
createdAt: comment.created_at,
|
|
186
|
+
body: veiled ? "" : (comment.body ?? ""),
|
|
187
|
+
staff: !veiled && comment.staff,
|
|
188
|
+
struck: comment.hidden && isAdmin && !veiled,
|
|
189
|
+
footer: actions.length > 0 ? actions : undefined,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
entries.sort((a, b) => a.at - b.at || a.order - b.order);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className={className} data-feedback-comment-thread="">
|
|
197
|
+
<FeedbackThread
|
|
198
|
+
messages={entries.map((entry) => entry.message)}
|
|
199
|
+
loading={loading}
|
|
200
|
+
now={now}
|
|
201
|
+
labels={{ thread: labels.comments, empty: labels.empty }}
|
|
202
|
+
/>
|
|
203
|
+
{onComment && (
|
|
204
|
+
<ChatComposer
|
|
205
|
+
className="mt-3"
|
|
206
|
+
onSend={(body) => onComment(body)}
|
|
207
|
+
pending={pending}
|
|
208
|
+
commit={commit}
|
|
209
|
+
maxLength={FEEDBACK_COMMENT_MAX}
|
|
210
|
+
showCount
|
|
211
|
+
labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its
|
|
219
|
+
* own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called
|
|
220
|
+
* where there is something to delete. */
|
|
221
|
+
function DeleteCommentButton({
|
|
222
|
+
comment,
|
|
223
|
+
onDelete,
|
|
224
|
+
commit,
|
|
225
|
+
labels,
|
|
226
|
+
}: {
|
|
227
|
+
comment: FeedbackComment;
|
|
228
|
+
onDelete: (comment: FeedbackComment) => void | Promise<unknown>;
|
|
229
|
+
commit?: CommitScope;
|
|
230
|
+
labels: FeedbackCommentsLabels;
|
|
231
|
+
}) {
|
|
232
|
+
const confirm = useConfirm();
|
|
233
|
+
const remove = async () => {
|
|
234
|
+
const ok = await confirm({
|
|
235
|
+
title: labels.deleteTitle,
|
|
236
|
+
body: labels.deleteBody,
|
|
237
|
+
confirmLabel: labels.delete,
|
|
238
|
+
tone: "danger",
|
|
239
|
+
commit,
|
|
240
|
+
});
|
|
241
|
+
if (ok) await onDelete(comment);
|
|
242
|
+
};
|
|
243
|
+
return (
|
|
244
|
+
<Button
|
|
245
|
+
type="button"
|
|
246
|
+
variant="link"
|
|
247
|
+
tone="danger"
|
|
248
|
+
size="sm"
|
|
249
|
+
commit={commit}
|
|
250
|
+
onClick={() => void remove().catch(() => {})}
|
|
251
|
+
>
|
|
252
|
+
{labels.delete}
|
|
253
|
+
</Button>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -25,4 +25,5 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
|
|
|
25
25
|
submitHint: (apple) => (apple ? "⌘ Enter to send" : "Ctrl+Enter to send"),
|
|
26
26
|
cancel: "Cancel",
|
|
27
27
|
save: "Send",
|
|
28
|
+
publicNotice: "Other users will see the title and text (never your screenshots or files).",
|
|
28
29
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { Button } from "../components/button";
|
|
4
4
|
import { Input } from "../components/input";
|
|
@@ -93,6 +93,11 @@ export interface FeedbackDialogTextLabels {
|
|
|
93
93
|
submitHint: FeedbackSubmitHint;
|
|
94
94
|
cancel: string;
|
|
95
95
|
save: string;
|
|
96
|
+
/** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when
|
|
97
|
+
* reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,
|
|
98
|
+
* so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH "Andere Personen
|
|
99
|
+
* sehen Titel und Text (nie Ihre Screenshots oder Dateien)." */
|
|
100
|
+
publicNotice?: string;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
// Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).
|
|
@@ -161,6 +166,14 @@ interface FeedbackDialogBaseProps {
|
|
|
161
166
|
/** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.
|
|
162
167
|
* Default `"md"`; `"sm"` draws the paperclip and camera at 14px. */
|
|
163
168
|
attachmentButtonSize?: ButtonSize;
|
|
169
|
+
/**
|
|
170
|
+
* A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md
|
|
171
|
+
* §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, "Other users
|
|
172
|
+
* will see the title and text (never your screenshots or files)", which
|
|
173
|
+
* `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with
|
|
174
|
+
* `aria-describedby`, so it is heard where the text is written. Left out: no line.
|
|
175
|
+
*/
|
|
176
|
+
publicNotice?: ReactNode;
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
/** One attachment — a screenshot, a picked or a pasted image; a second replaces
|
|
@@ -228,6 +241,7 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
|
|
|
228
241
|
requireBody = true,
|
|
229
242
|
attachmentButtonVariant,
|
|
230
243
|
attachmentButtonSize,
|
|
244
|
+
publicNotice,
|
|
231
245
|
} = props;
|
|
232
246
|
const [title, setTitle] = useState("");
|
|
233
247
|
const [body, setBody] = useState("");
|
|
@@ -236,6 +250,8 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
|
|
|
236
250
|
const [screenshot, setScreenshot] = useState<File | null>(null);
|
|
237
251
|
const [files, setFiles] = useState<File[]>([]);
|
|
238
252
|
const isMobile = usePhoneLayout();
|
|
253
|
+
const noticeId = useId();
|
|
254
|
+
const hasNotice = publicNotice !== undefined && publicNotice !== null && publicNotice !== false && publicNotice !== "";
|
|
239
255
|
const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
|
|
240
256
|
const attachmentText = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
|
|
241
257
|
// Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which
|
|
@@ -295,14 +311,26 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
|
|
|
295
311
|
const subjectField = (
|
|
296
312
|
<Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
|
|
297
313
|
);
|
|
298
|
-
const
|
|
314
|
+
const bodyTextarea = (
|
|
299
315
|
<Textarea
|
|
300
316
|
rows={5}
|
|
301
317
|
label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}
|
|
302
318
|
value={body}
|
|
303
319
|
onChange={(e) => setBody(e.target.value)}
|
|
320
|
+
aria-describedby={hasNotice ? noticeId : undefined}
|
|
304
321
|
/>
|
|
305
322
|
);
|
|
323
|
+
// 0.38: who will read it, right under what they are writing (§9.2 decision 6).
|
|
324
|
+
const bodyField = hasNotice ? (
|
|
325
|
+
<div className="space-y-1">
|
|
326
|
+
{bodyTextarea}
|
|
327
|
+
<p id={noticeId} className="text-xs text-muted" data-feedback-public-notice="">
|
|
328
|
+
{publicNotice}
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
) : (
|
|
332
|
+
bodyTextarea
|
|
333
|
+
);
|
|
306
334
|
const fieldProps = {
|
|
307
335
|
labels: attachmentLabels,
|
|
308
336
|
accept: attachmentAccept,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Inbox, MessageSquare } from "lucide-react";
|
|
1
|
+
import { Inbox, MessageSquare, Users } from "lucide-react";
|
|
2
2
|
import type { UserAvatarBadge } from "../components/user-avatar";
|
|
3
3
|
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
4
4
|
import { TopBarActionMenu, type TopBarMenuEntry } from "../shell/topbar-action-menu";
|
|
@@ -25,6 +25,10 @@ export interface FeedbackMenuLabels {
|
|
|
25
25
|
/** The link to the inbox — an admin's one list link, in place of "My feedback".
|
|
26
26
|
* keksdose `feedback.view_all`. */
|
|
27
27
|
viewFeedback: string;
|
|
28
|
+
/** The link to every public report, {@link FeedbackMenuProps.publicHref} (0.38,
|
|
29
|
+
* docs/feedback-harmonization.md §9.2 decision 21). Optional, so a complete
|
|
30
|
+
* `UiKitLabels` typed before 0.38 still compiles. de-CH "Alle Rückmeldungen". */
|
|
31
|
+
allFeedback?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// Defined in ./feedback-menu-labels, which holds no component code (0.34, KS U8).
|
|
@@ -43,7 +47,7 @@ export interface FeedbackMenuProps {
|
|
|
43
47
|
* `Sparkles` Help assistant → `/assistant` and `MessagesSquare` Support chat →
|
|
44
48
|
* `/support` with its unread chip. Any {@link TopBarMenuEntry}; keep their `key`s clear
|
|
45
49
|
* of the menu's own (`BUG`, `IDEA`, `QUESTION`, `OTHER`, `feedback-divider`,
|
|
46
|
-
* `my-feedback`, `view-feedback`).
|
|
50
|
+
* `all-feedback`, `my-feedback`, `view-feedback`).
|
|
47
51
|
*/
|
|
48
52
|
extraEntries?: TopBarMenuEntry[];
|
|
49
53
|
/** The user's own reports, linked for everyone but an admin. Default `"/my-feedback"`
|
|
@@ -51,6 +55,15 @@ export interface FeedbackMenuProps {
|
|
|
51
55
|
myFeedbackHref?: string;
|
|
52
56
|
/** The admin inbox, linked for an admin. Default `"/feedback"` (§2.3). */
|
|
53
57
|
inboxHref?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,
|
|
60
|
+
* an **All feedback** row (`Users`, key `all-feedback`) above the list link, for
|
|
61
|
+
* everyone — the admin too, who reads what the others read there. keksdose:
|
|
62
|
+
* `"/all-feedback"`; "My feedback" stays as it is. Leave it out where the app has no
|
|
63
|
+
* public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a
|
|
64
|
+
* demo — §9.2 decisions 7, 8).
|
|
65
|
+
*/
|
|
66
|
+
publicHref?: string;
|
|
54
67
|
/** A dot on the trigger — keksdose's unread support replies (`tone: "danger"`). Its
|
|
55
68
|
* `label` joins the trigger's name ("Send feedback 2 unread"). The words are the app's. */
|
|
56
69
|
iconBadge?: UserAvatarBadge | null;
|
|
@@ -70,7 +83,8 @@ export interface FeedbackMenuProps {
|
|
|
70
83
|
* the category badges' own glyphs), each calling `onFile(category)`;
|
|
71
84
|
* 3. a divider, always;
|
|
72
85
|
* 4. the app's `extraEntries`;
|
|
73
|
-
* 5.
|
|
86
|
+
* 5. with `publicHref` (0.38), **All feedback** → the public list, for everyone;
|
|
87
|
+
* 6. ONE list link, `Inbox`: an admin's is "View feedback" → `/feedback`, everyone
|
|
74
88
|
* else's "My feedback" → `/my-feedback` — keksdose's (`top-bar.tsx:205-214`, feedback
|
|
75
89
|
* #331: admins get the management view, everyone else their own submissions).
|
|
76
90
|
*
|
|
@@ -96,6 +110,7 @@ export function FeedbackMenu({
|
|
|
96
110
|
extraEntries,
|
|
97
111
|
myFeedbackHref = "/my-feedback",
|
|
98
112
|
inboxHref = "/feedback",
|
|
113
|
+
publicHref,
|
|
99
114
|
iconBadge,
|
|
100
115
|
labels: labelsProp,
|
|
101
116
|
align,
|
|
@@ -114,6 +129,17 @@ export function FeedbackMenu({
|
|
|
114
129
|
}),
|
|
115
130
|
{ kind: "divider", key: "feedback-divider" },
|
|
116
131
|
...(extraEntries ?? []),
|
|
132
|
+
...(publicHref
|
|
133
|
+
? [
|
|
134
|
+
{
|
|
135
|
+
kind: "link",
|
|
136
|
+
key: "all-feedback",
|
|
137
|
+
to: publicHref,
|
|
138
|
+
icon: <Users className="size-4" />,
|
|
139
|
+
label: labels.allFeedback ?? DEFAULT_FEEDBACK_MENU_LABELS.allFeedback!,
|
|
140
|
+
} satisfies TopBarMenuEntry,
|
|
141
|
+
]
|
|
142
|
+
: []),
|
|
117
143
|
{
|
|
118
144
|
kind: "link",
|
|
119
145
|
key: isAdmin ? "view-feedback" : "my-feedback",
|