@eifi1/ui-kit 0.52.0 → 0.53.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 +47 -54
- package/dist/billing/cancel-subscription.d.ts +5 -14
- package/dist/billing/cancel-subscription.js +1 -3
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/plan-card.d.ts +5 -13
- package/dist/billing/plan-card.js +3 -11
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-switch.d.ts +5 -14
- package/dist/billing/plan-switch.js +1 -3
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +3 -14
- package/dist/chart.js +64 -11
- package/dist/chart.js.map +1 -1
- package/dist/components/date-field.d.ts +1 -1
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +1 -1
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +4 -17
- package/dist/components/date-range-picker.js +2 -2
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/nav-pills.d.ts +1 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +1 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart.d.ts +64 -19
- package/dist/components/series-chart.js +2 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +6 -1
- package/dist/components/treemap-labels.js +2 -2
- package/dist/components/treemap-labels.js.map +1 -1
- package/dist/components/treemap.d.ts +29 -4
- package/dist/components/treemap.js +22 -9
- package/dist/components/treemap.js.map +1 -1
- package/dist/data-table.d.ts +1 -6
- package/dist/data-table.js +1 -9
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-countdown.d.ts +0 -5
- package/dist/demo/demo-countdown.js +0 -4
- package/dist/demo/demo-countdown.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-mutation.d.ts +11 -23
- package/dist/feedback/feedback-mutation.js +1 -9
- package/dist/feedback/feedback-mutation.js.map +1 -1
- package/dist/feedback/feedback-public-list.d.ts +1 -6
- package/dist/feedback/feedback-public-list.js +1 -2
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +0 -8
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +3 -50
- package/dist/feedback/feedback-row-detail.js +0 -8
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +3 -19
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +0 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +6 -40
- package/dist/feedback.js +88 -46
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/defaults.js +0 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +2 -19
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -14
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +2 -30
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +2 -19
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +2 -19
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +2 -19
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +2 -20
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +2 -19
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +9 -4
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +0 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +15 -64
- package/dist/index.js +22 -102
- package/dist/index.js.map +1 -1
- package/dist/rhf/fields.d.ts +2 -2
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/search-index.d.ts +0 -6
- package/dist/search/search-index.js +0 -4
- package/dist/search/search-index.js.map +1 -1
- package/dist/search.d.ts +1 -8
- package/dist/search.js +2 -7
- package/dist/search.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +7 -21
- package/dist/shell/account-menu-language.js +1 -3
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +9 -23
- package/dist/shell/account-menu-text-size.js +2 -3
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/app-shell.d.ts +0 -10
- package/dist/shell/app-shell.js +1 -7
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/company-switcher.d.ts +1 -1
- package/dist/shell/company-switcher.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +1 -1
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +7 -29
- package/dist/shell.js +12 -42
- package/dist/shell.js.map +1 -1
- package/dist/sync.d.ts +2 -6
- package/dist/sync.js +2 -15
- package/dist/sync.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +0 -5
- package/dist/theme/chart-palette.js +0 -2
- package/dist/theme/chart-palette.js.map +1 -1
- package/package.json +1 -1
- package/src/billing/cancel-subscription.tsx +5 -16
- package/src/billing/plan-card.tsx +7 -24
- package/src/billing/plan-switch.tsx +5 -16
- package/src/chart.ts +62 -13
- package/src/components/date-field.tsx +1 -1
- package/src/components/date-picker.tsx +0 -1
- package/src/components/date-range-picker.tsx +6 -22
- package/src/components/nav-pills.tsx +1 -1
- package/src/components/series-chart-axes.ts +6 -1
- package/src/components/series-chart.tsx +78 -21
- package/src/components/treemap-labels.ts +11 -3
- package/src/components/treemap.tsx +65 -12
- package/src/data-table.ts +1 -11
- package/src/demo/demo-countdown.ts +3 -8
- package/src/feedback/feedback-mutation.ts +12 -33
- package/src/feedback/feedback-public-list.tsx +1 -7
- package/src/feedback/feedback-row-detail-labels.ts +0 -8
- package/src/feedback/feedback-row-detail.tsx +8 -58
- package/src/feedback/feedback-status-undo.ts +4 -18
- package/src/feedback/feedback-thread.tsx +0 -10
- package/src/feedback.ts +89 -48
- package/src/i18n/defaults.ts +0 -4
- package/src/i18n/german.ts +2 -19
- package/src/i18n/kit-labels.tsx +5 -14
- package/src/i18n/locales/es.ts +2 -19
- package/src/i18n/locales/fr.ts +2 -19
- package/src/i18n/locales/hu.ts +2 -19
- package/src/i18n/locales/it.ts +2 -20
- package/src/i18n/locales/zh.ts +2 -19
- package/src/i18n/review.ts +9 -4
- package/src/i18n/shared-labels.ts +0 -1
- package/src/index.ts +30 -112
- package/src/rhf/fields.tsx +2 -2
- package/src/search/search-index.ts +0 -15
- package/src/search.ts +8 -8
- package/src/shell/account-menu-language.tsx +7 -27
- package/src/shell/account-menu-text-size.tsx +13 -32
- package/src/shell/app-shell.tsx +0 -13
- package/src/shell/company-switcher.tsx +1 -1
- package/src/shell/dev-menu.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +1 -1
- package/src/shell.ts +17 -44
- package/src/sync.ts +2 -15
- package/src/theme/chart-palette.ts +0 -2
- package/tokens.css +4 -4
- package/dist/billing/dialog-close.d.ts +0 -29
- package/dist/billing/dialog-close.js +0 -8
- package/dist/billing/dialog-close.js.map +0 -1
- package/dist/components/field-sync-labels.d.ts +0 -3
- package/dist/components/field-sync-labels.js +0 -12
- package/dist/components/field-sync-labels.js.map +0 -1
- package/dist/components/field-sync.d.ts +0 -186
- package/dist/components/field-sync.js +0 -269
- package/dist/components/field-sync.js.map +0 -1
- package/dist/components/section-nav.d.ts +0 -44
- package/dist/components/section-nav.js +0 -33
- package/dist/components/section-nav.js.map +0 -1
- package/dist/components/sign-chip-labels.d.ts +0 -3
- package/dist/components/sign-chip-labels.js +0 -11
- package/dist/components/sign-chip-labels.js.map +0 -1
- package/dist/components/sign-chip.d.ts +0 -77
- package/dist/components/sign-chip.js +0 -47
- package/dist/components/sign-chip.js.map +0 -1
- package/dist/lib/isin.d.ts +0 -38
- package/dist/lib/isin.js +0 -36
- package/dist/lib/isin.js.map +0 -1
- package/dist/shell/role-switcher.d.ts +0 -28
- package/dist/shell/role-switcher.js +0 -31
- package/dist/shell/role-switcher.js.map +0 -1
- package/src/billing/dialog-close.ts +0 -34
- package/src/components/field-sync-labels.ts +0 -14
- package/src/components/field-sync.tsx +0 -504
- package/src/components/section-nav.tsx +0 -72
- package/src/components/sign-chip-labels.ts +0 -13
- package/src/components/sign-chip.tsx +0 -133
- package/src/lib/isin.ts +0 -72
- package/src/shell/role-switcher.tsx +0 -53
|
@@ -13,9 +13,6 @@ function minutesAt(expiry, now) {
|
|
|
13
13
|
if (!Number.isFinite(expiry)) return null;
|
|
14
14
|
return Math.max(0, Math.ceil((expiry - now) / MINUTE));
|
|
15
15
|
}
|
|
16
|
-
function demoCountdown(expiresAt, now) {
|
|
17
|
-
return countdownOf(minutesAt(expiryMs(expiresAt), now));
|
|
18
|
-
}
|
|
19
16
|
function countdownOf(minutesLeft) {
|
|
20
17
|
if (minutesLeft === null) {
|
|
21
18
|
return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };
|
|
@@ -73,7 +70,6 @@ function useDemoCountdown(expiresAt, { now = Date.now, onEnded } = {}) {
|
|
|
73
70
|
export {
|
|
74
71
|
DEMO_MINUTES_ONLY,
|
|
75
72
|
DEMO_WARNING_MINUTES,
|
|
76
|
-
demoCountdown,
|
|
77
73
|
useDemoCountdown
|
|
78
74
|
};
|
|
79
75
|
//# sourceMappingURL=demo-countdown.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/demo/demo-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from \"react\";\n\n/**\n * The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to\n * the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`\n * (`/auth/me`). The client never decodes the JWT.\n */\n\n/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a\n * `Date`. */\nexport type DemoExpiry = string | number | Date;\n\n/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */\nexport const DEMO_WARNING_MINUTES = 10;\n/** Below this many minutes the countdown shows minutes only (§5.4). */\nexport const DEMO_MINUTES_ONLY = 60;\n\nconst MINUTE = 60_000;\n\nexport interface DemoCountdown {\n /**\n * Whole minutes left, ROUNDED UP — 30 s left is \"1 min\", and 0 only once the demo has\n * ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and\n * never ends by itself (a parse slip must not throw a visitor out).\n */\n minutesLeft: number | null;\n /** The display split: whole hours, and the minutes past them. In the last hour\n * (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */\n hours: number;\n minutes: number;\n /** `minutesLeft <= 60`: show minutes only. */\n lastHour: boolean;\n /** `minutesLeft <= 10`: the warning tone. */\n warning: boolean;\n /** The demo's time is up. */\n ended: boolean;\n}\n\n/** `expiresAt` as epoch milliseconds, or NaN. */\nfunction expiryMs(expiresAt: DemoExpiry | null | undefined): number {\n if (expiresAt === null || expiresAt === undefined) return Number.NaN;\n if (typeof expiresAt === \"number\") return expiresAt;\n if (expiresAt instanceof Date) return expiresAt.getTime();\n return Date.parse(expiresAt);\n}\n\n/** Whole minutes left at `now`, rounded up, never below 0; null for an unreadable end. */\nfunction minutesAt(expiry: number, now: number): number | null {\n if (!Number.isFinite(expiry)) return null;\n return Math.max(0, Math.ceil((expiry - now) / MINUTE));\n}\n\n
|
|
1
|
+
{"version":3,"sources":["../../src/demo/demo-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from \"react\";\n\n/**\n * The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to\n * the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`\n * (`/auth/me`). The client never decodes the JWT.\n */\n\n/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a\n * `Date`. */\nexport type DemoExpiry = string | number | Date;\n\n/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */\nexport const DEMO_WARNING_MINUTES = 10;\n/** Below this many minutes the countdown shows minutes only (§5.4). */\nexport const DEMO_MINUTES_ONLY = 60;\n\nconst MINUTE = 60_000;\n\nexport interface DemoCountdown {\n /**\n * Whole minutes left, ROUNDED UP — 30 s left is \"1 min\", and 0 only once the demo has\n * ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and\n * never ends by itself (a parse slip must not throw a visitor out).\n */\n minutesLeft: number | null;\n /** The display split: whole hours, and the minutes past them. In the last hour\n * (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */\n hours: number;\n minutes: number;\n /** `minutesLeft <= 60`: show minutes only. */\n lastHour: boolean;\n /** `minutesLeft <= 10`: the warning tone. */\n warning: boolean;\n /** The demo's time is up. */\n ended: boolean;\n}\n\n/** `expiresAt` as epoch milliseconds, or NaN. */\nfunction expiryMs(expiresAt: DemoExpiry | null | undefined): number {\n if (expiresAt === null || expiresAt === undefined) return Number.NaN;\n if (typeof expiresAt === \"number\") return expiresAt;\n if (expiresAt instanceof Date) return expiresAt.getTime();\n return Date.parse(expiresAt);\n}\n\n/** Whole minutes left at `now`, rounded up, never below 0; null for an unreadable end. */\nfunction minutesAt(expiry: number, now: number): number | null {\n if (!Number.isFinite(expiry)) return null;\n return Math.max(0, Math.ceil((expiry - now) / MINUTE));\n}\n\n/** The countdown for `minutesLeft` — see {@link DemoCountdown} for the rounding and the\n * thresholds. (Public as `demoCountdown(expiresAt, now)` until 0.53, deprecated in 0.52,\n * API-4: `useDemoCountdown` with its `now` option is the same countdown.) */\nfunction countdownOf(minutesLeft: number | null): DemoCountdown {\n if (minutesLeft === null) {\n return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };\n }\n const lastHour = minutesLeft <= DEMO_MINUTES_ONLY;\n return {\n minutesLeft,\n hours: lastHour ? 0 : Math.floor(minutesLeft / 60),\n minutes: lastHour ? minutesLeft : minutesLeft % 60,\n lastHour,\n warning: minutesLeft <= DEMO_WARNING_MINUTES,\n ended: minutesLeft === 0,\n };\n}\n\nexport interface DemoCountdownOptions {\n /**\n * The clock, in epoch milliseconds. Default `Date.now`. A test passes its own (or uses\n * fake timers, which move `Date.now` too). Keep it a stable function — a new one each\n * render re-arms the timer each render.\n */\n now?: () => number;\n /** Called once when the countdown reaches zero — also on mount, if it already has.\n * The app clears the session and replaces to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n}\n\n/**\n * The time left to `expiresAt`, re-rendering when the shown minute changes — one timer\n * to the next minute boundary, not a tick a second — and once more when the tab comes\n * back from the background (a hidden tab's timers are throttled).\n *\n * `onEnded` fires at zero (§5.5). The countdown is the client's, from `expires_at`; the\n * server's answer to a request after the end (a 401) ends the demo as well — the app's\n * 401 handler, not this hook.\n */\nexport function useDemoCountdown(\n expiresAt: DemoExpiry | null | undefined,\n { now = Date.now, onEnded }: DemoCountdownOptions = {},\n): DemoCountdown {\n const expiry = expiryMs(expiresAt);\n const subscribe = useCallback(\n (notify: () => void) => {\n if (!Number.isFinite(expiry)) return () => {};\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n const left = expiry - now();\n if (left <= 0) return;\n // The rounded-up minute changes when `left` crosses the next whole minute below.\n const delay = left - (Math.ceil(left / MINUTE) - 1) * MINUTE;\n timer = setTimeout(() => {\n notify();\n arm();\n }, delay);\n };\n const onVisible = () => {\n if (document.visibilityState !== \"visible\") return;\n clearTimeout(timer);\n notify();\n arm();\n };\n arm();\n document.addEventListener(\"visibilitychange\", onVisible);\n return () => {\n clearTimeout(timer);\n document.removeEventListener(\"visibilitychange\", onVisible);\n };\n },\n [expiry, now],\n );\n const snapshot = () => minutesAt(expiry, now());\n const minutesLeft = useSyncExternalStore(subscribe, snapshot, snapshot);\n const countdown = countdownOf(minutesLeft);\n\n const fireEnded = useEffectEvent(() => onEnded?.());\n useEffect(() => {\n if (countdown.ended) fireEnded();\n }, [countdown.ended]);\n\n return countdown;\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,gBAAgB,4BAA4B;AAatE,MAAM,uBAAuB;AAE7B,MAAM,oBAAoB;AAEjC,MAAM,SAAS;AAsBf,SAAS,SAAS,WAAkD;AAClE,MAAI,cAAc,QAAQ,cAAc,OAAW,QAAO,OAAO;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO;AAC1C,MAAI,qBAAqB,KAAM,QAAO,UAAU,QAAQ;AACxD,SAAO,KAAK,MAAM,SAAS;AAC7B;AAGA,SAAS,UAAU,QAAgB,KAA4B;AAC7D,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACrC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,OAAO,MAAM,CAAC;AACvD;AAKA,SAAS,YAAY,aAA2C;AAC9D,MAAI,gBAAgB,MAAM;AACxB,WAAO,EAAE,aAAa,OAAO,GAAG,SAAS,GAAG,UAAU,OAAO,SAAS,OAAO,OAAO,MAAM;AAAA,EAC5F;AACA,QAAM,WAAW,eAAe;AAChC,SAAO;AAAA,IACL;AAAA,IACA,OAAO,WAAW,IAAI,KAAK,MAAM,cAAc,EAAE;AAAA,IACjD,SAAS,WAAW,cAAc,cAAc;AAAA,IAChD;AAAA,IACA,SAAS,eAAe;AAAA,IACxB,OAAO,gBAAgB;AAAA,EACzB;AACF;AAuBO,SAAS,iBACd,WACA,EAAE,MAAM,KAAK,KAAK,QAAQ,IAA0B,CAAC,GACtC;AACf,QAAM,SAAS,SAAS,SAAS;AACjC,QAAM,YAAY;AAAA,IAChB,CAAC,WAAuB;AACtB,UAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO,MAAM;AAAA,MAAC;AAC5C,UAAI;AACJ,YAAM,MAAM,MAAM;AAChB,cAAM,OAAO,SAAS,IAAI;AAC1B,YAAI,QAAQ,EAAG;AAEf,cAAM,QAAQ,QAAQ,KAAK,KAAK,OAAO,MAAM,IAAI,KAAK;AACtD,gBAAQ,WAAW,MAAM;AACvB,iBAAO;AACP,cAAI;AAAA,QACN,GAAG,KAAK;AAAA,MACV;AACA,YAAM,YAAY,MAAM;AACtB,YAAI,SAAS,oBAAoB,UAAW;AAC5C,qBAAa,KAAK;AAClB,eAAO;AACP,YAAI;AAAA,MACN;AACA,UAAI;AACJ,eAAS,iBAAiB,oBAAoB,SAAS;AACvD,aAAO,MAAM;AACX,qBAAa,KAAK;AAClB,iBAAS,oBAAoB,oBAAoB,SAAS;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,GAAG;AAAA,EACd;AACA,QAAM,WAAW,MAAM,UAAU,QAAQ,IAAI,CAAC;AAC9C,QAAM,cAAc,qBAAqB,WAAW,UAAU,QAAQ;AACtE,QAAM,YAAY,YAAY,WAAW;AAEzC,QAAM,YAAY,eAAe,MAAM,UAAU,CAAC;AAClD,YAAU,MAAM;AACd,QAAI,UAAU,MAAO,WAAU;AAAA,EACjC,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}
|
package/dist/eslint/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const roleUtilities = {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
const VERSION = true ? "0.
|
|
50
|
+
const VERSION = true ? "0.53.0" : "0.0.0-source";
|
|
51
51
|
const recommended = {
|
|
52
52
|
name: "@eifi1/ui-kit/recommended",
|
|
53
53
|
// Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
|
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A page's mutation
|
|
3
|
-
*
|
|
2
|
+
* A page's mutation, run once (refactor audit 2026-10-30, FLW-3): TanStack Query's
|
|
3
|
+
* `mutateAsync`, or any `(patch) => Promise` that resolves once the PATCH has landed.
|
|
4
4
|
*
|
|
5
|
-
* The parts
|
|
6
|
-
* v5 calls per-call callbacks for the LATEST `mutate()` on
|
|
7
|
-
* overwrites the options and detaches from the earlier mutation —
|
|
8
|
-
* before the first PATCH answered (another swipe, another pill,
|
|
9
|
-
* first change's Undo toast, and the detail's editor stayed
|
|
10
|
-
* `mutateAsync` returns settles for every call, whatever the observer
|
|
5
|
+
* The parts took TanStack Query's `mutate` with a per-call `onSuccess` until 0.53
|
|
6
|
+
* (deprecated in 0.52). TanStack v5 calls per-call callbacks for the LATEST `mutate()` on
|
|
7
|
+
* an observer only — it overwrites the options and detaches from the earlier mutation —
|
|
8
|
+
* so a second change sent before the first PATCH answered (another swipe, another pill,
|
|
9
|
+
* the Undo itself) lost the first change's Undo toast, and the detail's editor stayed
|
|
10
|
+
* open. The promise `mutateAsync` returns settles for every call, whatever the observer
|
|
11
|
+
* does next.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* taken from whichever comes first: the callback, or the promise the call returned. A
|
|
14
|
-
* rejected promise is the mutation's own to report (its `onError`), so it is caught here
|
|
13
|
+
* A rejected promise is the mutation's own to report (its `onError`), so it is caught here
|
|
15
14
|
* and nothing more happens.
|
|
16
15
|
*
|
|
17
16
|
* @internal Not part of `/feedback` or the barrel.
|
|
18
17
|
*/
|
|
19
|
-
|
|
20
|
-
* A mutation in either form: the promise form (0.52) — TanStack's `mutateAsync`, any
|
|
21
|
-
* `(patch) => Promise` — or the callback form before it — TanStack's `mutate`, a function
|
|
22
|
-
* that calls `onSuccess` once the PATCH has landed, and only then. One signature for
|
|
23
|
-
* both, so an inline function handed to a prop of this type gets its parameter types (a
|
|
24
|
-
* union of the two leaves `async (patch) => …` an implicit `any`).
|
|
25
|
-
*/
|
|
26
|
-
export type FeedbackMutationCall<P> = (patch: P, callbacks: {
|
|
27
|
-
onSuccess: () => void;
|
|
28
|
-
}) => void | Promise<unknown>;
|
|
29
|
-
/** Send `patch` through `call`, and run `landed` once when it has landed. */
|
|
30
|
-
export declare function runMutation<P>(call: FeedbackMutationCall<P>, patch: P, landed: () => void): void;
|
|
18
|
+
export declare function runMutation<P>(call: (patch: P) => Promise<unknown>, patch: P, landed: () => void): void;
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { isThenable } from "../lib/thenable.js";
|
|
3
2
|
function runMutation(call, patch, landed) {
|
|
4
|
-
|
|
5
|
-
const once = () => {
|
|
6
|
-
if (done) return;
|
|
7
|
-
done = true;
|
|
8
|
-
landed();
|
|
9
|
-
};
|
|
10
|
-
const answer = call(patch, { onSuccess: once });
|
|
11
|
-
if (isThenable(answer)) answer.then(once, () => void 0);
|
|
3
|
+
call(patch).then(landed, () => void 0);
|
|
12
4
|
}
|
|
13
5
|
export {
|
|
14
6
|
runMutation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-mutation.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-mutation.ts"],"sourcesContent":["/**\n * A page's mutation, run once (refactor audit 2026-10-30, FLW-3): TanStack Query's\n * `mutateAsync`, or any `(patch) => Promise` that resolves once the PATCH has landed.\n *\n * The parts took TanStack Query's `mutate` with a per-call `onSuccess` until 0.53\n * (deprecated in 0.52). TanStack v5 calls per-call callbacks for the LATEST `mutate()` on\n * an observer only — it overwrites the options and detaches from the earlier mutation —\n * so a second change sent before the first PATCH answered (another swipe, another pill,\n * the Undo itself) lost the first change's Undo toast, and the detail's editor stayed\n * open. The promise `mutateAsync` returns settles for every call, whatever the observer\n * does next.\n *\n * A rejected promise is the mutation's own to report (its `onError`), so it is caught here\n * and nothing more happens.\n *\n * @internal Not part of `/feedback` or the barrel.\n */\nexport function runMutation<P>(call: (patch: P) => Promise<unknown>, patch: P, landed: () => void): void {\n call(patch).then(landed, () => undefined);\n}\n"],"mappings":";AAiBO,SAAS,YAAe,MAAsC,OAAU,QAA0B;AACvG,OAAK,KAAK,EAAE,KAAK,QAAQ,MAAM,MAAS;AAC1C;","names":[]}
|
|
@@ -43,11 +43,6 @@ export interface FeedbackPublicListProps<T extends FeedbackPublicListItem = Feed
|
|
|
43
43
|
* what it carries: every other `onOpenChange` in the kit is a boolean (refactor audit
|
|
44
44
|
* 2026-10-30, API-5). */
|
|
45
45
|
onOpenIdChange?: (id: number | null) => void;
|
|
46
|
-
/**
|
|
47
|
-
* @deprecated 0.52 — `onOpenIdChange`, the same call: an `onOpenChange` is a boolean
|
|
48
|
-
* everywhere else in the kit; removed in 0.53
|
|
49
|
-
*/
|
|
50
|
-
onOpenChange?: (id: number | null) => void;
|
|
51
46
|
/**
|
|
52
47
|
* 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older
|
|
53
48
|
* one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's
|
|
@@ -105,7 +100,7 @@ export interface FeedbackPublicListProps<T extends FeedbackPublicListItem = Feed
|
|
|
105
100
|
* next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from
|
|
106
101
|
* another page. Needs a router, as every `DataTable` does.
|
|
107
102
|
*/
|
|
108
|
-
export declare function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({ rows, total, sort, onSortChange, search, onSearchChange, renderDetail, onVote, loading, openId: openIdProp, onOpenIdChange,
|
|
103
|
+
export declare function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({ rows, total, sort, onSortChange, search, onSearchChange, renderDetail, onVote, loading, openId: openIdProp, onOpenIdChange, openRow, page: pageIndex, hasMore, onPageChange, commit, renderDate: renderDateProp, labels: labelsProp, pageLabels: pageLabelsProp, fillHeight, className, }: FeedbackPublicListProps<T>): import("react").JSX.Element;
|
|
109
104
|
export interface FeedbackPublicDetailProps {
|
|
110
105
|
/** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by
|
|
111
106
|
* the server, §9.4) and the replies. */
|
|
@@ -111,7 +111,6 @@ function FeedbackPublicList({
|
|
|
111
111
|
loading = false,
|
|
112
112
|
openId: openIdProp,
|
|
113
113
|
onOpenIdChange,
|
|
114
|
-
onOpenChange,
|
|
115
114
|
openRow,
|
|
116
115
|
page: pageIndex = 0,
|
|
117
116
|
hasMore = false,
|
|
@@ -131,7 +130,7 @@ function FeedbackPublicList({
|
|
|
131
130
|
const openId = openIdProp !== void 0 ? openIdProp : ownOpenId;
|
|
132
131
|
const setOpen = (id) => {
|
|
133
132
|
if (openIdProp === void 0) setOwnOpenId(id);
|
|
134
|
-
|
|
133
|
+
onOpenIdChange?.(id);
|
|
135
134
|
};
|
|
136
135
|
const shownRows = openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows;
|
|
137
136
|
const columns = useMemo(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\nimport { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { IconButton } from \"../components/icon-button\";\nimport { SearchField } from \"../components/search-field\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackProse,\n FeedbackStatusBadge,\n} from \"./feedback-inbox\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FeedbackCommentThread, type FeedbackCommentsLabels } from \"./feedback-comment-thread\";\nimport type { FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\nimport type {\n FeedbackComment,\n FeedbackPublicListItem,\n FeedbackPublicRecord,\n FeedbackPublicSort,\n FeedbackVoteState,\n} from \"./feedback-public-types\";\nimport {\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport { FeedbackVoteButton } from \"./feedback-vote-button\";\n\nexport { DEFAULT_FEEDBACK_PUBLIC_LABELS };\nexport type { FeedbackPublicLabels };\n\n/** `feedbackPublic` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackPublicLabels(labels?: LabelOverride<FeedbackPublicLabels>): FeedbackPublicLabels {\n return useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS, labels);\n}\n\n/** The sort toggle's order: the server's default first (§9.6). */\nconst SORTS: readonly FeedbackPublicSort[] = [\"newest\", \"votes\", \"updated\"];\n\n/** A date in a cell, as the inbox writes it: `<time>`, nowrap, tabular figures. */\nfunction PublicDate({ value, renderDate }: { value: string | null | undefined; renderDate: FeedbackRenderDate }) {\n if (!value) return <span className=\"text-placeholder\">{EMPTY_FORMATTED_VALUE}</span>;\n return (\n <time dateTime={value} className=\"whitespace-nowrap tabular-nums text-muted\">\n {renderDate(value)}\n </time>\n );\n}\n\n/** The visible comments as a small chip beside the title — the count in figures, the\n * words for a screen reader. Nothing at 0. */\nfunction CommentCount({ count, labels }: { count: number; labels: FeedbackPublicLabels }) {\n if (!count) return null;\n return (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted\" data-feedback-comment-count={count}>\n <MessageSquare className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.comments(count)}</span>\n </span>\n );\n}\n\n/** The votes on a phone card: the thumb and the count, in the brand colour when the\n * viewer's own. */\nfunction VoteCount({ count, voted, labels }: { count: number; voted: boolean; labels: FeedbackPublicLabels }) {\n return (\n <span\n className={cn(\"ms-auto inline-flex shrink-0 items-center gap-1 text-xs\", voted ? \"text-brand\" : \"text-muted\")}\n data-feedback-vote-count={count}\n >\n <ThumbsUp className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.votes(count)}</span>\n </span>\n );\n}\n\n/** 0.38.1: previous / next over the server's pages when it answers no total\n * (`GET /feedback/public` — Kurvenschmiede): \"Page 3\" between two arrows. The arrows'\n * names are `dataTable`'s, the strip's; the page's is `feedbackPublic.page`. */\nfunction PublicPager({\n page,\n hasMore,\n onPageChange,\n labels,\n}: {\n page: number;\n hasMore: boolean;\n onPageChange: (page: number) => void;\n labels: FeedbackPublicLabels;\n}) {\n const tableLabels = useKitLabels(\"dataTable\", DEFAULT_DATA_TABLE_LABELS);\n const name = labels.page(page + 1);\n return (\n <div\n role=\"group\"\n aria-label={name}\n className=\"flex items-center justify-end gap-2 text-sm text-secondary\"\n data-feedback-public-pager={page}\n >\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.prevPage}\n disabled={page <= 0}\n onClick={() => onPageChange(page - 1)}\n >\n {/* \"Previous\" points back along the line — left in LTR, right in RTL. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <span className=\"tabular-nums\" aria-hidden>\n {name}\n </span>\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.nextPage}\n disabled={!hasMore}\n onClick={() => onPageChange(page + 1)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n );\n}\n\n/* ── The list ─────────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicListProps<T extends FeedbackPublicListItem = FeedbackPublicListItem> {\n /** `GET /feedback/public?sort=&q=` — the server's page, in the server's order.\n * `readonly` since 0.52 (refactor audit 2026-10-30, API-7): the list never changes it. */\n rows: readonly T[];\n /** How many the query has in all — the line beside the sort. Left out, no line. */\n total?: number;\n /** The server sorts (§9.6): the toggle only says which way. */\n sort: FeedbackPublicSort;\n onSortChange: (sort: FeedbackPublicSort) => void;\n /** The search box's text (`q`, titles only). */\n search?: string;\n /**\n * The search box is drawn ONLY with this handler: where the app's titles are sealed\n * (Kurvenschmiede, `FeedbackDeps.public_search=False`, §9.9) there is nothing to search\n * on and the server answers `q` with a 422 — so it passes none.\n */\n onSearchChange?: (search: string) => void;\n /** The opened row: the app's {@link FeedbackPublicDetail} with the row's\n * `GET /feedback/public/{id}` and its comments. */\n renderDetail: (row: T) => ReactNode;\n /** `PUT` / `DELETE /feedback/{id}/vote` for a row's button (`next`: the new state);\n * resolve with the answer, reject to roll back. */\n onVote: (row: T, next: boolean) => Promise<FeedbackVoteState>;\n /** The page is loading: the table says so where its rows would be. */\n loading?: boolean;\n /** The opened row's id, when the app keeps it (a `?row=` param). Left out, the list\n * keeps it itself. */\n openId?: number | null;\n /** 0.52: a row was opened (its id) or closed (`null`) — `openId`'s pair. Named for\n * what it carries: every other `onOpenChange` in the kit is a boolean (refactor audit\n * 2026-10-30, API-5). */\n onOpenIdChange?: (id: number | null) => void;\n /**\n * @deprecated 0.52 — `onOpenIdChange`, the same call: an `onOpenChange` is a boolean\n * everywhere else in the kit; removed in 0.53\n */\n onOpenChange?: (id: number | null) => void;\n /**\n * 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older\n * one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's\n * fields and more) and passes it with `openId`; while `openId` is its id it is drawn\n * first, above the page's rows, and opened with `renderDetail` (on the phone, in the\n * full-screen dialog). Closed, it is gone. A row the page has already is never drawn\n * twice: the page's own is opened instead.\n */\n openRow?: T | null;\n /**\n * 0.38.1: the server's page, from 0 — for a pager where `/feedback/public` answers no\n * total (Kurvenschmiede). With {@link onPageChange} a **Previous / Page n / Next**\n * strip stands under the table; Next is off without {@link hasMore}. Left out, no\n * pager: the server's `limit` is all there is.\n */\n page?: number;\n /** 0.38.1: there is a page after this one (e.g. the page came back full). */\n hasMore?: boolean;\n /** 0.38.1: go to another page (`page ± 1`); the app fetches it. Draws the pager. */\n onPageChange?: (page: number) => void;\n /** A vote COMMITS — see Button's `commit`. */\n commit?: CommitScope;\n /** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */\n renderDate?: FeedbackRenderDate;\n /** Over the `feedbackPublic` namespace. The shared column names (#, Date, Category,\n * Subject, Status) are `feedbackPage`'s: pass `pageLabels` for those. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n pageLabels?: LabelOverride<FeedbackPageLabels>;\n /** 0.40.1: the list fills the window as DataTable's `fillHeight` (page layout §1) — the\n * public feedback page passes §1.2's rule. Below md the page scrolls, as everywhere. */\n fillHeight?: boolean;\n className?: string;\n}\n\n/**\n * **All feedback** — the reports every user the app lets in may read (round 0.38,\n * docs/feedback-harmonization.md §9.3, §9.9; keksdose's `/all-feedback`).\n *\n * - **Titles only** (decision 23): #, Date, Category, Subject (with the visible\n * comments), Votes, Status. No body, no submitter, no file — the list item carries\n * none of them (§9.10).\n * - **The server sorts**: the toggle **Newest / Most wanted / Recently updated** sets\n * `sort`, and the columns themselves do not sort (a page of 50 sorted in the browser\n * would not be the 50 the sort asks for).\n * - **The search box only with `onSearchChange`**: an app whose titles are sealed\n * passes none (§9.9).\n * - **Votes** are `FeedbackVoteButton`s, optimistic, the viewer's own report pressed\n * and locked (\"Your report\", §9.6).\n * - A row opens in place with `renderDetail` — on the phone, in a full-screen dialog.\n * The phone cards carry the title, the status, the category, the date and the two\n * counts; the vote itself is in the opened report (a button inside the card, which is\n * the row's own button, would be two controls in one).\n *\n * No client paging: the server's `limit` is the page, and `total` says how many there\n * are. For more than one page, `page` / `hasMore` / `onPageChange` draw a previous /\n * next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from\n * another page. Needs a router, as every `DataTable` does.\n */\nexport function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({\n rows,\n total,\n sort,\n onSortChange,\n search = \"\",\n onSearchChange,\n renderDetail,\n onVote,\n loading = false,\n openId: openIdProp,\n onOpenIdChange,\n onOpenChange,\n openRow,\n page: pageIndex = 0,\n hasMore = false,\n onPageChange,\n commit,\n renderDate: renderDateProp,\n labels: labelsProp,\n pageLabels: pageLabelsProp,\n fillHeight,\n className,\n}: FeedbackPublicListProps<T>) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const page = useFeedbackPageLabels(pageLabelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n const [ownOpenId, setOwnOpenId] = useState<number | null>(null);\n const openId = openIdProp !== undefined ? openIdProp : ownOpenId;\n const setOpen = (id: number | null) => {\n if (openIdProp === undefined) setOwnOpenId(id);\n (onOpenIdChange ?? onOpenChange)?.(id);\n };\n // A `?row=` report from another page: first, while it is the open one, and only when\n // the page does not have it itself.\n // `as T[]`: DataTable's `rows` is still mutable in its type (API-7), and never mutated.\n const shownRows = (\n openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows\n ) as T[];\n\n const columns = useMemo<DataTableColumn<T>[]>(\n () => [\n {\n key: \"id\",\n header: page.columnId,\n cell: (row) => <span className=\"font-mono text-xs text-muted\">{row.id}</span>,\n mobileHidden: true,\n },\n {\n key: \"date\",\n header: page.columnDate,\n cell: (row) => <PublicDate value={row.created_at} renderDate={renderDate} />,\n },\n {\n key: \"category\",\n header: page.columnCategory,\n cell: (row) => <FeedbackCategoryBadge category={row.category} label={categoryLabels[row.category]} />,\n },\n {\n key: \"title\",\n header: page.columnSubject,\n cell: (row) => (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-2 large:items-start\">\n <span className=\"min-w-0 truncate-until-large font-medium\">{row.title}</span>\n <CommentCount count={row.comment_count} labels={labels} />\n </span>\n ),\n mobilePrimary: true,\n },\n {\n key: \"votes\",\n header: labels.columnVotes,\n className: \"whitespace-nowrap\",\n // The cell is a button: the row's own link must never wrap it.\n noRowLink: true,\n cell: (row) => (\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={(next) => onVote(row, next)}\n commit={commit}\n size=\"sm\"\n />\n ),\n },\n {\n key: \"status\",\n header: page.columnStatus,\n className: \"whitespace-nowrap\",\n cell: (row) => <FeedbackStatusBadge status={row.status} />,\n },\n ],\n [page, labels, categoryLabels, renderDate, onVote, commit],\n );\n\n return (\n <div\n className={cn(\"flex flex-col gap-3\", fillHeight && FILL_HEIGHT_CLASS, className)}\n data-feedback-public-list=\"\"\n aria-busy={loading || undefined}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <ToggleGroup\n aria-label={labels.sort}\n value={sort}\n onChange={(value) => onSortChange(value)}\n options={SORTS.map((value) => ({\n value,\n label: value === \"newest\" ? labels.sortNewest : value === \"votes\" ? labels.sortVotes : labels.sortUpdated,\n }))}\n />\n {onSearchChange && (\n <SearchField\n aria-label={labels.search}\n placeholder={labels.search}\n value={search}\n onChange={onSearchChange}\n className=\"min-w-[12rem] flex-1 md:max-w-xs\"\n />\n )}\n {total !== undefined && (\n <span className=\"ms-auto text-sm text-muted\" data-feedback-public-total={total}>\n {labels.total(total)}\n </span>\n )}\n </div>\n <DataTable\n rows={shownRows}\n columns={columns}\n rowKey={(row) => row.id}\n rowName={(row) => row.title}\n onRowClick={(row) => setOpen(openId === row.id ? null : row.id)}\n isExpanded={(row) => row.id === openId}\n expandedRow={(row) => renderDetail(row)}\n mobileExpandAsDialog\n paginated={false}\n fillHeight={fillHeight}\n empty={loading ? labels.loading : labels.empty}\n mobileCard={(row) => (\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 flex-1 font-medium [overflow-wrap:anywhere]\">{row.title}</span>\n <FeedbackStatusBadge status={row.status} className=\"shrink-0\" />\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted\">\n <FeedbackCategoryBadge category={row.category} compact label={categoryLabels[row.category]} />\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <CommentCount count={row.comment_count} labels={labels} />\n {/* A count, not the button: the whole card is the row's button, and a button\n inside it is two controls in one. The vote is in the opened report. */}\n <VoteCount count={row.votes} voted={row.voted || row.mine} labels={labels} />\n </div>\n </div>\n )}\n />\n {onPageChange && (pageIndex > 0 || hasMore) && (\n <PublicPager page={pageIndex} hasMore={hasMore} onPageChange={onPageChange} labels={labels} />\n )}\n </div>\n );\n}\n\n/* ── The detail ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicDetailProps {\n /** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by\n * the server, §9.4) and the replies. */\n row: FeedbackPublicRecord;\n /** `GET /feedback/{id}/comments`, oldest first; `undefined` while it loads. */\n comments: readonly FeedbackComment[] | undefined;\n /** `PUT` / `DELETE /feedback/{id}/vote`; resolve with the answer, reject to roll back. */\n onVote: (next: boolean) => Promise<FeedbackVoteState>;\n /** The viewer is the app's admin: Hide / Unhide on the comments. */\n isAdmin?: boolean;\n /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47 (§12): `POST /feedback/attachments` — the comment box then takes one file,\n * pasted, captured or picked, as the rework note does. Left out, text only. */\n onUpload?: FeedbackEntryUploadOptions[\"onUpload\"];\n /** 0.47: the comment box's \"Capture screenshot\". */\n onCaptureScreenshot?: FeedbackEntryUploadOptions[\"onCaptureScreenshot\"];\n /** 0.47: the comment box's file field, over the contract's defaults. */\n attachment?: FeedbackEntryUploadOptions[\"attachment\"];\n /** 0.47: the app's authenticated GET for the comments' pictures, drawn inline. */\n fetcher?: AuthedFetcher | null;\n /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The vote and the thread's buttons COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** Voting is off for this viewer (a demo): `true`, or the reason. */\n voteDisabled?: boolean | string;\n /** See {@link FeedbackRenderDate}. */\n renderDate?: FeedbackRenderDate;\n /** \"Now\" for the thread's relative times — pin it in tests and stories. */\n now?: Date | number;\n /** Over the `feedbackPublic` namespace. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n /** Over the `feedbackComments` namespace, for the thread. */\n commentLabels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\n/**\n * One public report opened (round 0.38, docs/feedback-harmonization.md §9.3, §9.10): who\n * filed it — the first name the server sent, else **\"A user\"** (§9.5) — and when; the\n * **vote**; the **public body**; and the **conversation**, the admin's replies and other\n * people's comments merged oldest first (§9.8, `FeedbackCommentThread`), with the box to\n * add one.\n *\n * What it never draws, because the public record never carries it (§9.10): an address,\n * a last name, the report's screenshot or files, the page the report was filed on. The\n * body is the server's `public_body` — the client strips nothing and adds nothing. The\n * one file it shows (0.47, §12) is a comment's own, posted with it to be seen.\n */\nexport function FeedbackPublicDetail({\n row,\n comments,\n onVote,\n isAdmin = false,\n onComment,\n onHide,\n onDelete,\n onUpload,\n onCaptureScreenshot,\n attachment,\n fetcher,\n pending,\n commit,\n voteDisabled,\n renderDate: renderDateProp,\n now,\n labels: labelsProp,\n commentLabels,\n className,\n}: FeedbackPublicDetailProps) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const renderDate = useFeedbackRenderDate(renderDateProp);\n return (\n <FeedbackDetail className={className}>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2\" data-feedback-public-detail={row.id}>\n <span className=\"text-sm text-secondary\" data-feedback-author=\"\">\n {labels.filedBy(row.author_name ?? labels.anonymous)}\n </span>\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={onVote}\n disabled={voteDisabled}\n commit={commit}\n className=\"ms-auto\"\n />\n </div>\n <FeedbackDetailSection title={labels.body}>\n {/* Rework rounds stay in it — the author's own words (§9.4) — on a server before\n server-kit 0.13; since then each is an entry in the conversation (§10.3). */}\n <FeedbackProse>{row.body}</FeedbackProse>\n </FeedbackDetailSection>\n <FeedbackDetailSection title={labels.conversation}>\n <FeedbackCommentThread\n replies={row.replies}\n comments={comments ?? []}\n loading={comments === undefined}\n isAdmin={isAdmin}\n // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.\n isAuthor={row.mine}\n onComment={onComment}\n onHide={onHide}\n onDelete={onDelete}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n attachment={attachment}\n fetcher={fetcher}\n pending={pending}\n commit={commit}\n now={now}\n labels={commentLabels}\n />\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\nimport { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { IconButton } from \"../components/icon-button\";\nimport { SearchField } from \"../components/search-field\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackProse,\n FeedbackStatusBadge,\n} from \"./feedback-inbox\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FeedbackCommentThread, type FeedbackCommentsLabels } from \"./feedback-comment-thread\";\nimport type { FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\nimport type {\n FeedbackComment,\n FeedbackPublicListItem,\n FeedbackPublicRecord,\n FeedbackPublicSort,\n FeedbackVoteState,\n} from \"./feedback-public-types\";\nimport {\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport { FeedbackVoteButton } from \"./feedback-vote-button\";\n\nexport { DEFAULT_FEEDBACK_PUBLIC_LABELS };\nexport type { FeedbackPublicLabels };\n\n/** `feedbackPublic` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackPublicLabels(labels?: LabelOverride<FeedbackPublicLabels>): FeedbackPublicLabels {\n return useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS, labels);\n}\n\n/** The sort toggle's order: the server's default first (§9.6). */\nconst SORTS: readonly FeedbackPublicSort[] = [\"newest\", \"votes\", \"updated\"];\n\n/** A date in a cell, as the inbox writes it: `<time>`, nowrap, tabular figures. */\nfunction PublicDate({ value, renderDate }: { value: string | null | undefined; renderDate: FeedbackRenderDate }) {\n if (!value) return <span className=\"text-placeholder\">{EMPTY_FORMATTED_VALUE}</span>;\n return (\n <time dateTime={value} className=\"whitespace-nowrap tabular-nums text-muted\">\n {renderDate(value)}\n </time>\n );\n}\n\n/** The visible comments as a small chip beside the title — the count in figures, the\n * words for a screen reader. Nothing at 0. */\nfunction CommentCount({ count, labels }: { count: number; labels: FeedbackPublicLabels }) {\n if (!count) return null;\n return (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted\" data-feedback-comment-count={count}>\n <MessageSquare className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.comments(count)}</span>\n </span>\n );\n}\n\n/** The votes on a phone card: the thumb and the count, in the brand colour when the\n * viewer's own. */\nfunction VoteCount({ count, voted, labels }: { count: number; voted: boolean; labels: FeedbackPublicLabels }) {\n return (\n <span\n className={cn(\"ms-auto inline-flex shrink-0 items-center gap-1 text-xs\", voted ? \"text-brand\" : \"text-muted\")}\n data-feedback-vote-count={count}\n >\n <ThumbsUp className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.votes(count)}</span>\n </span>\n );\n}\n\n/** 0.38.1: previous / next over the server's pages when it answers no total\n * (`GET /feedback/public` — Kurvenschmiede): \"Page 3\" between two arrows. The arrows'\n * names are `dataTable`'s, the strip's; the page's is `feedbackPublic.page`. */\nfunction PublicPager({\n page,\n hasMore,\n onPageChange,\n labels,\n}: {\n page: number;\n hasMore: boolean;\n onPageChange: (page: number) => void;\n labels: FeedbackPublicLabels;\n}) {\n const tableLabels = useKitLabels(\"dataTable\", DEFAULT_DATA_TABLE_LABELS);\n const name = labels.page(page + 1);\n return (\n <div\n role=\"group\"\n aria-label={name}\n className=\"flex items-center justify-end gap-2 text-sm text-secondary\"\n data-feedback-public-pager={page}\n >\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.prevPage}\n disabled={page <= 0}\n onClick={() => onPageChange(page - 1)}\n >\n {/* \"Previous\" points back along the line — left in LTR, right in RTL. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <span className=\"tabular-nums\" aria-hidden>\n {name}\n </span>\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.nextPage}\n disabled={!hasMore}\n onClick={() => onPageChange(page + 1)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n );\n}\n\n/* ── The list ─────────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicListProps<T extends FeedbackPublicListItem = FeedbackPublicListItem> {\n /** `GET /feedback/public?sort=&q=` — the server's page, in the server's order.\n * `readonly` since 0.52 (refactor audit 2026-10-30, API-7): the list never changes it. */\n rows: readonly T[];\n /** How many the query has in all — the line beside the sort. Left out, no line. */\n total?: number;\n /** The server sorts (§9.6): the toggle only says which way. */\n sort: FeedbackPublicSort;\n onSortChange: (sort: FeedbackPublicSort) => void;\n /** The search box's text (`q`, titles only). */\n search?: string;\n /**\n * The search box is drawn ONLY with this handler: where the app's titles are sealed\n * (Kurvenschmiede, `FeedbackDeps.public_search=False`, §9.9) there is nothing to search\n * on and the server answers `q` with a 422 — so it passes none.\n */\n onSearchChange?: (search: string) => void;\n /** The opened row: the app's {@link FeedbackPublicDetail} with the row's\n * `GET /feedback/public/{id}` and its comments. */\n renderDetail: (row: T) => ReactNode;\n /** `PUT` / `DELETE /feedback/{id}/vote` for a row's button (`next`: the new state);\n * resolve with the answer, reject to roll back. */\n onVote: (row: T, next: boolean) => Promise<FeedbackVoteState>;\n /** The page is loading: the table says so where its rows would be. */\n loading?: boolean;\n /** The opened row's id, when the app keeps it (a `?row=` param). Left out, the list\n * keeps it itself. */\n openId?: number | null;\n /** 0.52: a row was opened (its id) or closed (`null`) — `openId`'s pair. Named for\n * what it carries: every other `onOpenChange` in the kit is a boolean (refactor audit\n * 2026-10-30, API-5). */\n onOpenIdChange?: (id: number | null) => void;\n /**\n * 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older\n * one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's\n * fields and more) and passes it with `openId`; while `openId` is its id it is drawn\n * first, above the page's rows, and opened with `renderDetail` (on the phone, in the\n * full-screen dialog). Closed, it is gone. A row the page has already is never drawn\n * twice: the page's own is opened instead.\n */\n openRow?: T | null;\n /**\n * 0.38.1: the server's page, from 0 — for a pager where `/feedback/public` answers no\n * total (Kurvenschmiede). With {@link onPageChange} a **Previous / Page n / Next**\n * strip stands under the table; Next is off without {@link hasMore}. Left out, no\n * pager: the server's `limit` is all there is.\n */\n page?: number;\n /** 0.38.1: there is a page after this one (e.g. the page came back full). */\n hasMore?: boolean;\n /** 0.38.1: go to another page (`page ± 1`); the app fetches it. Draws the pager. */\n onPageChange?: (page: number) => void;\n /** A vote COMMITS — see Button's `commit`. */\n commit?: CommitScope;\n /** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */\n renderDate?: FeedbackRenderDate;\n /** Over the `feedbackPublic` namespace. The shared column names (#, Date, Category,\n * Subject, Status) are `feedbackPage`'s: pass `pageLabels` for those. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n pageLabels?: LabelOverride<FeedbackPageLabels>;\n /** 0.40.1: the list fills the window as DataTable's `fillHeight` (page layout §1) — the\n * public feedback page passes §1.2's rule. Below md the page scrolls, as everywhere. */\n fillHeight?: boolean;\n className?: string;\n}\n\n/**\n * **All feedback** — the reports every user the app lets in may read (round 0.38,\n * docs/feedback-harmonization.md §9.3, §9.9; keksdose's `/all-feedback`).\n *\n * - **Titles only** (decision 23): #, Date, Category, Subject (with the visible\n * comments), Votes, Status. No body, no submitter, no file — the list item carries\n * none of them (§9.10).\n * - **The server sorts**: the toggle **Newest / Most wanted / Recently updated** sets\n * `sort`, and the columns themselves do not sort (a page of 50 sorted in the browser\n * would not be the 50 the sort asks for).\n * - **The search box only with `onSearchChange`**: an app whose titles are sealed\n * passes none (§9.9).\n * - **Votes** are `FeedbackVoteButton`s, optimistic, the viewer's own report pressed\n * and locked (\"Your report\", §9.6).\n * - A row opens in place with `renderDetail` — on the phone, in a full-screen dialog.\n * The phone cards carry the title, the status, the category, the date and the two\n * counts; the vote itself is in the opened report (a button inside the card, which is\n * the row's own button, would be two controls in one).\n *\n * No client paging: the server's `limit` is the page, and `total` says how many there\n * are. For more than one page, `page` / `hasMore` / `onPageChange` draw a previous /\n * next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from\n * another page. Needs a router, as every `DataTable` does.\n */\nexport function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({\n rows,\n total,\n sort,\n onSortChange,\n search = \"\",\n onSearchChange,\n renderDetail,\n onVote,\n loading = false,\n openId: openIdProp,\n onOpenIdChange,\n openRow,\n page: pageIndex = 0,\n hasMore = false,\n onPageChange,\n commit,\n renderDate: renderDateProp,\n labels: labelsProp,\n pageLabels: pageLabelsProp,\n fillHeight,\n className,\n}: FeedbackPublicListProps<T>) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const page = useFeedbackPageLabels(pageLabelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n const [ownOpenId, setOwnOpenId] = useState<number | null>(null);\n const openId = openIdProp !== undefined ? openIdProp : ownOpenId;\n const setOpen = (id: number | null) => {\n if (openIdProp === undefined) setOwnOpenId(id);\n onOpenIdChange?.(id);\n };\n // A `?row=` report from another page: first, while it is the open one, and only when\n // the page does not have it itself.\n // `as T[]`: DataTable's `rows` is still mutable in its type (API-7), and never mutated.\n const shownRows = (\n openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows\n ) as T[];\n\n const columns = useMemo<DataTableColumn<T>[]>(\n () => [\n {\n key: \"id\",\n header: page.columnId,\n cell: (row) => <span className=\"font-mono text-xs text-muted\">{row.id}</span>,\n mobileHidden: true,\n },\n {\n key: \"date\",\n header: page.columnDate,\n cell: (row) => <PublicDate value={row.created_at} renderDate={renderDate} />,\n },\n {\n key: \"category\",\n header: page.columnCategory,\n cell: (row) => <FeedbackCategoryBadge category={row.category} label={categoryLabels[row.category]} />,\n },\n {\n key: \"title\",\n header: page.columnSubject,\n cell: (row) => (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-2 large:items-start\">\n <span className=\"min-w-0 truncate-until-large font-medium\">{row.title}</span>\n <CommentCount count={row.comment_count} labels={labels} />\n </span>\n ),\n mobilePrimary: true,\n },\n {\n key: \"votes\",\n header: labels.columnVotes,\n className: \"whitespace-nowrap\",\n // The cell is a button: the row's own link must never wrap it.\n noRowLink: true,\n cell: (row) => (\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={(next) => onVote(row, next)}\n commit={commit}\n size=\"sm\"\n />\n ),\n },\n {\n key: \"status\",\n header: page.columnStatus,\n className: \"whitespace-nowrap\",\n cell: (row) => <FeedbackStatusBadge status={row.status} />,\n },\n ],\n [page, labels, categoryLabels, renderDate, onVote, commit],\n );\n\n return (\n <div\n className={cn(\"flex flex-col gap-3\", fillHeight && FILL_HEIGHT_CLASS, className)}\n data-feedback-public-list=\"\"\n aria-busy={loading || undefined}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <ToggleGroup\n aria-label={labels.sort}\n value={sort}\n onChange={(value) => onSortChange(value)}\n options={SORTS.map((value) => ({\n value,\n label: value === \"newest\" ? labels.sortNewest : value === \"votes\" ? labels.sortVotes : labels.sortUpdated,\n }))}\n />\n {onSearchChange && (\n <SearchField\n aria-label={labels.search}\n placeholder={labels.search}\n value={search}\n onChange={onSearchChange}\n className=\"min-w-[12rem] flex-1 md:max-w-xs\"\n />\n )}\n {total !== undefined && (\n <span className=\"ms-auto text-sm text-muted\" data-feedback-public-total={total}>\n {labels.total(total)}\n </span>\n )}\n </div>\n <DataTable\n rows={shownRows}\n columns={columns}\n rowKey={(row) => row.id}\n rowName={(row) => row.title}\n onRowClick={(row) => setOpen(openId === row.id ? null : row.id)}\n isExpanded={(row) => row.id === openId}\n expandedRow={(row) => renderDetail(row)}\n mobileExpandAsDialog\n paginated={false}\n fillHeight={fillHeight}\n empty={loading ? labels.loading : labels.empty}\n mobileCard={(row) => (\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 flex-1 font-medium [overflow-wrap:anywhere]\">{row.title}</span>\n <FeedbackStatusBadge status={row.status} className=\"shrink-0\" />\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted\">\n <FeedbackCategoryBadge category={row.category} compact label={categoryLabels[row.category]} />\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <CommentCount count={row.comment_count} labels={labels} />\n {/* A count, not the button: the whole card is the row's button, and a button\n inside it is two controls in one. The vote is in the opened report. */}\n <VoteCount count={row.votes} voted={row.voted || row.mine} labels={labels} />\n </div>\n </div>\n )}\n />\n {onPageChange && (pageIndex > 0 || hasMore) && (\n <PublicPager page={pageIndex} hasMore={hasMore} onPageChange={onPageChange} labels={labels} />\n )}\n </div>\n );\n}\n\n/* ── The detail ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicDetailProps {\n /** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by\n * the server, §9.4) and the replies. */\n row: FeedbackPublicRecord;\n /** `GET /feedback/{id}/comments`, oldest first; `undefined` while it loads. */\n comments: readonly FeedbackComment[] | undefined;\n /** `PUT` / `DELETE /feedback/{id}/vote`; resolve with the answer, reject to roll back. */\n onVote: (next: boolean) => Promise<FeedbackVoteState>;\n /** The viewer is the app's admin: Hide / Unhide on the comments. */\n isAdmin?: boolean;\n /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47 (§12): `POST /feedback/attachments` — the comment box then takes one file,\n * pasted, captured or picked, as the rework note does. Left out, text only. */\n onUpload?: FeedbackEntryUploadOptions[\"onUpload\"];\n /** 0.47: the comment box's \"Capture screenshot\". */\n onCaptureScreenshot?: FeedbackEntryUploadOptions[\"onCaptureScreenshot\"];\n /** 0.47: the comment box's file field, over the contract's defaults. */\n attachment?: FeedbackEntryUploadOptions[\"attachment\"];\n /** 0.47: the app's authenticated GET for the comments' pictures, drawn inline. */\n fetcher?: AuthedFetcher | null;\n /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The vote and the thread's buttons COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** Voting is off for this viewer (a demo): `true`, or the reason. */\n voteDisabled?: boolean | string;\n /** See {@link FeedbackRenderDate}. */\n renderDate?: FeedbackRenderDate;\n /** \"Now\" for the thread's relative times — pin it in tests and stories. */\n now?: Date | number;\n /** Over the `feedbackPublic` namespace. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n /** Over the `feedbackComments` namespace, for the thread. */\n commentLabels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\n/**\n * One public report opened (round 0.38, docs/feedback-harmonization.md §9.3, §9.10): who\n * filed it — the first name the server sent, else **\"A user\"** (§9.5) — and when; the\n * **vote**; the **public body**; and the **conversation**, the admin's replies and other\n * people's comments merged oldest first (§9.8, `FeedbackCommentThread`), with the box to\n * add one.\n *\n * What it never draws, because the public record never carries it (§9.10): an address,\n * a last name, the report's screenshot or files, the page the report was filed on. The\n * body is the server's `public_body` — the client strips nothing and adds nothing. The\n * one file it shows (0.47, §12) is a comment's own, posted with it to be seen.\n */\nexport function FeedbackPublicDetail({\n row,\n comments,\n onVote,\n isAdmin = false,\n onComment,\n onHide,\n onDelete,\n onUpload,\n onCaptureScreenshot,\n attachment,\n fetcher,\n pending,\n commit,\n voteDisabled,\n renderDate: renderDateProp,\n now,\n labels: labelsProp,\n commentLabels,\n className,\n}: FeedbackPublicDetailProps) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const renderDate = useFeedbackRenderDate(renderDateProp);\n return (\n <FeedbackDetail className={className}>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2\" data-feedback-public-detail={row.id}>\n <span className=\"text-sm text-secondary\" data-feedback-author=\"\">\n {labels.filedBy(row.author_name ?? labels.anonymous)}\n </span>\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={onVote}\n disabled={voteDisabled}\n commit={commit}\n className=\"ms-auto\"\n />\n </div>\n <FeedbackDetailSection title={labels.body}>\n {/* Rework rounds stay in it — the author's own words (§9.4) — on a server before\n server-kit 0.13; since then each is an entry in the conversation (§10.3). */}\n <FeedbackProse>{row.body}</FeedbackProse>\n </FeedbackDetailSection>\n <FeedbackDetailSection title={labels.conversation}>\n <FeedbackCommentThread\n replies={row.replies}\n comments={comments ?? []}\n loading={comments === undefined}\n isAdmin={isAdmin}\n // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.\n isAuthor={row.mine}\n onComment={onComment}\n onHide={onHide}\n onDelete={onDelete}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n attachment={attachment}\n fetcher={fetcher}\n pending={pending}\n commit={commit}\n now={now}\n labels={commentLabels}\n />\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -14,14 +14,6 @@ const DEFAULT_FEEDBACK_DETAIL_LABELS = {
|
|
|
14
14
|
outcome: "Outcome",
|
|
15
15
|
replies: "Replies",
|
|
16
16
|
resolvedAt: (date) => `Resolved: ${date}`,
|
|
17
|
-
// The outcome editor's three words, read by nothing since 0.38 and "kept for one
|
|
18
|
-
// release" for 13 minors: no tag, so the deprecations test never listed them (refactor
|
|
19
|
-
// audit 2026-10-30, DOC-9). Deprecated on `FeedbackDetailLabels`, the type an app reads
|
|
20
|
-
// them through — a tag on these keys reached no reader (0.52 code review). The keys
|
|
21
|
-
// leave the type and all seven catalogues in 0.53.
|
|
22
|
-
outcomeAdd: "Add outcome",
|
|
23
|
-
outcomeUpdate: "Update",
|
|
24
|
-
outcomePlaceholder: "What was done, decided, or why this won't be addressed.",
|
|
25
17
|
rework: "Rework",
|
|
26
18
|
reworkTitle: "Send for rework",
|
|
27
19
|
reworkSend: "Send rework",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-row-detail-labels.ts"],"sourcesContent":["// The English words of ./feedback-row-detail, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-row-detail\n// re-exports them.\n\nimport type { FeedbackDetailLabels } from \"./feedback-row-detail\";\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n replies: \"Replies\",\n resolvedAt: (date) => `Resolved: ${date}`,\n
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-row-detail-labels.ts"],"sourcesContent":["// The English words of ./feedback-row-detail, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-row-detail\n// re-exports them.\n\nimport type { FeedbackDetailLabels } from \"./feedback-row-detail\";\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n replies: \"Replies\",\n resolvedAt: (date) => `Resolved: ${date}`,\n rework: \"Rework\",\n reworkTitle: \"Send for rework\",\n reworkSend: \"Send rework\",\n reworkPlaceholder: \"What still needs refinement? Any new constraints or change of direction.\",\n reworkUploadFailed: \"The file could not be uploaded. The rework was not sent.\",\n status: \"Status\",\n note: \"Note\",\n};\n"],"mappings":";AAOO,MAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,UAAU,CAAC,SAAS,YAAY,IAAI;AAAA,EACpC,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY,CAAC,SAAS,aAAa,IAAI;AAAA,EACvC,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,MAAM;AACR;","names":[]}
|
|
@@ -4,7 +4,6 @@ import type { PathPattern } from "../landing/routing.js";
|
|
|
4
4
|
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
5
5
|
import type { FeedbackNoteAttachment, FeedbackStatus } from "./feedback-inbox.js";
|
|
6
6
|
import type { FeedbackContext, FeedbackRecord } from "./feedback-record.js";
|
|
7
|
-
import { type FeedbackMutationCall } from "./feedback-mutation.js";
|
|
8
7
|
import type { FeedbackStatusChange } from "./feedback-status-undo.js";
|
|
9
8
|
import type { CommitScope } from "../components/write-lock.js";
|
|
10
9
|
import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels.js";
|
|
@@ -91,18 +90,6 @@ export interface FeedbackDetailLabels {
|
|
|
91
90
|
/** On section 4's heading line once the row is settled, given the formatted
|
|
92
91
|
* `resolved_at` date. de-CH: "Erledigt am: {{date}}" */
|
|
93
92
|
resolvedAt: (date: string) => string;
|
|
94
|
-
/** The admin's button while there is no outcome — read by nothing since 0.38 (the
|
|
95
|
-
* outcome is frozen, §9.7; a reply is written instead). de-CH: "Ergebnis hinzufügen"
|
|
96
|
-
* @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */
|
|
97
|
-
outcomeAdd: string;
|
|
98
|
-
/** The same button once there is one. Unused since 0.38, as `outcomeAdd`.
|
|
99
|
-
* de-CH: "Aktualisieren"
|
|
100
|
-
* @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */
|
|
101
|
-
outcomeUpdate: string;
|
|
102
|
-
/** The line above the outcome editor. Unused since 0.38, as `outcomeAdd`.
|
|
103
|
-
* de-CH: "Was wurde umgesetzt, entschieden oder warum nicht."
|
|
104
|
-
* @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */
|
|
105
|
-
outcomePlaceholder: string;
|
|
106
93
|
/** The button that opens section 5 on an answered row. de-CH: "Nacharbeit" */
|
|
107
94
|
rework: string;
|
|
108
95
|
/** Section 5's heading. de-CH: "Zur Nacharbeit senden" */
|
|
@@ -127,30 +114,9 @@ export interface FeedbackDetailLabels {
|
|
|
127
114
|
export { DEFAULT_FEEDBACK_DETAIL_LABELS };
|
|
128
115
|
/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */
|
|
129
116
|
export declare function useFeedbackDetailLabels(labels?: LabelOverride<FeedbackDetailLabels>): FeedbackDetailLabels;
|
|
130
|
-
/**
|
|
131
|
-
* The page a report was filed on as the reporter had it — path + query + hash (keksdose
|
|
132
|
-
* `localHref`: the query and hash carry the state, `?tab=import`, a drill-down). Read from
|
|
133
|
-
* `context.url`, else `context.route`; a path-only `url` works. `null` when there is no
|
|
134
|
-
* page.
|
|
135
|
-
*
|
|
136
|
-
* Always a path on THIS app, never another origin's address — but the path and query
|
|
137
|
-
* are the reporter's to write, so it is TEXT, not a link target: `/join?token=…` acts on
|
|
138
|
-
* load, and `/login?next=…` forwards a signed-in admin to whatever its query names
|
|
139
|
-
* (refactor audit 2026-10-30, FLW-7; 0.52 code review). The kit's URL cell and section
|
|
140
|
-
* link {@link feedbackPagePath} alone, and not even that for a one-shot route
|
|
141
|
-
* (`pageLinkExclude`) — `FeedbackRowDetail` and `useFeedbackColumns` draw the page that
|
|
142
|
-
* way; an app's own cell should not link this.
|
|
143
|
-
*/
|
|
144
|
-
export declare function feedbackPageHref(context: FeedbackContext | null | undefined): string | null;
|
|
145
117
|
/** The page's path alone — the compact text a URL column shows (§4.3), and the only part
|
|
146
118
|
* of the page the kit links. */
|
|
147
119
|
export declare function feedbackPagePath(context: FeedbackContext | null | undefined): string | null;
|
|
148
|
-
/**
|
|
149
|
-
* What "Copy URL" copies: the FULL `context.url` as the reporter's browser had it —
|
|
150
|
-
* origin included, which is the one part that says which copy of the app it was
|
|
151
|
-
* (feedback #278) — else `context.route`. `null` when there is no page.
|
|
152
|
-
*/
|
|
153
|
-
export declare function feedbackPageUrl(context: FeedbackContext | null | undefined): string | null;
|
|
154
120
|
/**
|
|
155
121
|
* Every file a report carries, in the order the detail shows them (§4.4.3): the
|
|
156
122
|
* screenshot it was filed with, then its other files, then whatever the rework rounds
|
|
@@ -218,18 +184,6 @@ export interface FeedbackDetailPatch {
|
|
|
218
184
|
* `feedbackToast.updateFailed` (§4.4) — and leaves the editor open with its draft.
|
|
219
185
|
*/
|
|
220
186
|
export type FeedbackDetailUpdateAsync = (patch: FeedbackDetailPatch) => Promise<unknown>;
|
|
221
|
-
/**
|
|
222
|
-
* The callback form: TanStack Query's `mutate`, or a function that calls `onSuccess` once
|
|
223
|
-
* the PATCH has landed. TanStack calls a per-call `onSuccess` for the LATEST `mutate()`
|
|
224
|
-
* only, so an editor whose save was followed by another PATCH before it answered stayed
|
|
225
|
-
* open, and the earlier status change lost its Undo.
|
|
226
|
-
*
|
|
227
|
-
* @deprecated 0.52 — pass `update.mutateAsync` ({@link FeedbackDetailUpdateAsync}); removed
|
|
228
|
-
* in 0.53
|
|
229
|
-
*/
|
|
230
|
-
export type FeedbackDetailUpdate = (patch: FeedbackDetailPatch, callbacks: {
|
|
231
|
-
onSuccess: () => void;
|
|
232
|
-
}) => void;
|
|
233
187
|
/** `POST /feedback/attachments` (§3.3): one file up, its URL
|
|
234
188
|
* (`/api/v1/feedback/attachments/<key>`) back. Reject on failure. */
|
|
235
189
|
export type FeedbackUpload = (file: File) => Promise<string>;
|
|
@@ -247,7 +201,7 @@ export interface FeedbackReworkSectionProps {
|
|
|
247
201
|
/** The PATCH — see {@link FeedbackDetailUpdateAsync}. Called with `{ id, body }` only,
|
|
248
202
|
* and only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still
|
|
249
203
|
* turns into an entry for one release. Optional since 0.40; pass one of the two. */
|
|
250
|
-
onUpdate?:
|
|
204
|
+
onUpdate?: FeedbackDetailUpdateAsync;
|
|
251
205
|
/** The PATCH is in flight (`update.isPending`). The upload adds its own. */
|
|
252
206
|
pending?: boolean;
|
|
253
207
|
/** The upload. Left out, the note goes without a file field. */
|
|
@@ -300,9 +254,8 @@ export interface FeedbackRowDetailProps {
|
|
|
300
254
|
/** The signed-in user's id — who the AUTHOR is: the description editor and rework. */
|
|
301
255
|
viewerId?: number | null;
|
|
302
256
|
/** The page's PATCH — `update.mutateAsync` (see {@link FeedbackDetailUpdateAsync}). The
|
|
303
|
-
* callback form
|
|
304
|
-
|
|
305
|
-
onUpdate: FeedbackMutationCall<FeedbackDetailPatch>;
|
|
257
|
+
* callback form, `update.mutate`, went in 0.53 (deprecated in 0.52). */
|
|
258
|
+
onUpdate: FeedbackDetailUpdateAsync;
|
|
306
259
|
/** `update.isPending`: the editors' save buttons wait for it. */
|
|
307
260
|
pending?: boolean;
|
|
308
261
|
/** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with
|
|
@@ -44,15 +44,9 @@ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels
|
|
|
44
44
|
function useFeedbackDetailLabels(labels) {
|
|
45
45
|
return useKitLabels("feedbackDetail", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);
|
|
46
46
|
}
|
|
47
|
-
function feedbackPageHref(context) {
|
|
48
|
-
return feedbackPage(context)?.href ?? null;
|
|
49
|
-
}
|
|
50
47
|
function feedbackPagePath(context) {
|
|
51
48
|
return feedbackPage(context)?.path ?? null;
|
|
52
49
|
}
|
|
53
|
-
function feedbackPageUrl(context) {
|
|
54
|
-
return feedbackPage(context)?.url ?? null;
|
|
55
|
-
}
|
|
56
50
|
function feedbackAttachmentUrls(row) {
|
|
57
51
|
const urls = [
|
|
58
52
|
...row.screenshot_url ? [row.screenshot_url] : [],
|
|
@@ -383,9 +377,7 @@ export {
|
|
|
383
377
|
FeedbackReworkSection,
|
|
384
378
|
FeedbackRowDetail,
|
|
385
379
|
feedbackAttachmentUrls,
|
|
386
|
-
feedbackPageHref,
|
|
387
380
|
feedbackPagePath,
|
|
388
|
-
feedbackPageUrl,
|
|
389
381
|
feedbackRowAccess,
|
|
390
382
|
useFeedbackDetailLabels
|
|
391
383
|
};
|