@eifi1/ui-kit 0.46.0 → 0.47.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 +12 -11
- package/dist/admin/user-roster.d.ts +6 -2
- package/dist/admin/user-roster.js +7 -3
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +22 -5
- package/dist/components/data-table-row-actions.js +14 -5
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table.d.ts +16 -1
- package/dist/components/data-table.js +20 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/row-actions.d.ts +37 -5
- package/dist/components/row-actions.js +22 -5
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/tabs.d.ts +24 -16
- package/dist/components/tabs.js +37 -31
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review.js +3 -1
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui-labels.js +1 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js +2 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-download.d.ts +19 -0
- package/dist/feedback/feedback-attachment-download.js +59 -0
- package/dist/feedback/feedback-attachment-download.js.map +1 -0
- package/dist/feedback/feedback-comment-thread.d.ts +17 -6
- package/dist/feedback/feedback-comment-thread.js +21 -2
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-entry-file.d.ts +61 -0
- package/dist/feedback/feedback-entry-file.js +76 -0
- package/dist/feedback/feedback-entry-file.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +18 -5
- package/dist/feedback/feedback-public-list.js +8 -0
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +7 -2
- package/dist/feedback/feedback-replies.d.ts +26 -9
- package/dist/feedback/feedback-replies.js +20 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +0 -1
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +23 -10
- package/dist/feedback/feedback-row-detail.js +10 -54
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/german.js +1 -6
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -6
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +1 -6
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +1 -6
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +1 -6
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +1 -6
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +1 -6
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +0 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/dist/notifications/device-zone-report.d.ts +7 -0
- package/dist/notifications/device-zone-report.js +8 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/feedback-notification-types.d.ts +14 -1
- package/dist/notifications/feedback-notification-types.js +5 -1
- package/dist/notifications/feedback-notification-types.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +1 -1
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +12 -3
- package/dist/notifications/notification-inbox.js +2 -2
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +0 -3
- package/dist/notifications/notifications-labels.d.ts +2 -13
- package/dist/notifications/notifications-labels.js +0 -4
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +3 -8
- package/dist/notifications/quiet-hours-setting.js +8 -3
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/offline/local-first.d.ts +9 -0
- package/dist/offline/local-first.js +14 -0
- package/dist/offline/local-first.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +2 -1
- package/dist/offline.js.map +1 -1
- package/dist/sync/db.d.ts +17 -0
- package/dist/sync/db.js +10 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.d.ts +11 -9
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/hooks.d.ts +5 -0
- package/dist/sync/hooks.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +5 -2
- package/dist/sync.js.map +1 -1
- package/dist/theme/theme-store.d.ts +4 -4
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/package.json +2 -2
- package/src/admin/user-roster.tsx +12 -3
- package/src/components/data-table-row-actions.tsx +33 -9
- package/src/components/data-table.tsx +46 -3
- package/src/components/row-actions.tsx +73 -10
- package/src/components/tabs.tsx +66 -44
- package/src/components/translation-review.tsx +3 -1
- package/src/components/ui-labels.ts +1 -1
- package/src/data-table.ts +3 -0
- package/src/feedback/feedback-attachment-download.tsx +77 -0
- package/src/feedback/feedback-comment-thread.tsx +42 -7
- package/src/feedback/feedback-entry-file.tsx +150 -0
- package/src/feedback/feedback-public-list.tsx +25 -4
- package/src/feedback/feedback-public-types.ts +7 -2
- package/src/feedback/feedback-replies.tsx +46 -19
- package/src/feedback/feedback-row-detail-labels.ts +0 -1
- package/src/feedback/feedback-row-detail.tsx +35 -86
- package/src/feedback.ts +1 -0
- package/src/i18n/german.ts +1 -6
- package/src/i18n/locales/es.ts +1 -6
- package/src/i18n/locales/fr.ts +1 -6
- package/src/i18n/locales/hu.ts +1 -6
- package/src/i18n/locales/it.ts +1 -6
- package/src/i18n/locales/zh.ts +1 -6
- package/src/i18n/review.ts +0 -1
- package/src/index.ts +19 -5
- package/src/notifications/device-zone-report.ts +14 -1
- package/src/notifications/feedback-notification-types.ts +19 -1
- package/src/notifications/notification-bell.tsx +1 -1
- package/src/notifications/notification-inbox.tsx +15 -5
- package/src/notifications/notification-types.ts +0 -3
- package/src/notifications/notifications-labels.ts +2 -17
- package/src/notifications/quiet-hours-setting.tsx +16 -11
- package/src/offline/local-first.ts +24 -0
- package/src/offline.ts +1 -1
- package/src/sync/db.ts +22 -0
- package/src/sync/engine.ts +11 -9
- package/src/sync/hooks.ts +5 -0
- package/src/sync.ts +2 -2
- package/src/theme/theme-store.ts +13 -5
- package/dist/notifications/time-zone-select.d.ts +0 -53
- package/dist/notifications/time-zone-select.js +0 -76
- package/dist/notifications/time-zone-select.js.map +0 -1
- package/src/notifications/time-zone-select.tsx +0 -129
|
@@ -609,6 +609,14 @@ export interface DataTableProps<T> {
|
|
|
609
609
|
* `mobileSwipeActions` wins over the swipes derived from here.
|
|
610
610
|
*/
|
|
611
611
|
rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
|
|
612
|
+
/**
|
|
613
|
+
* The `rowActions` column's heading (0.46.1, keksdose dev#598 rework — Marcel: a
|
|
614
|
+
* VISIBLE "Actions" heading on every table with an actions column). Default: the
|
|
615
|
+
* provider's `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A
|
|
616
|
+
* node replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader
|
|
617
|
+
* only.
|
|
618
|
+
*/
|
|
619
|
+
rowActionsHeader?: ReactNode | false;
|
|
612
620
|
/**
|
|
613
621
|
* The bar above the table — a search field, filter chips, a create button. Inside
|
|
614
622
|
* the table's frame, above the header, on desktop and phone alike (resource-list
|
|
@@ -703,6 +711,13 @@ export interface DataTableDesktopSwipeActions<T> {
|
|
|
703
711
|
* no commit scope (the plan decides).
|
|
704
712
|
*/
|
|
705
713
|
commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);
|
|
714
|
+
/**
|
|
715
|
+
* The column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE "Actions"
|
|
716
|
+
* heading on every table with an actions column). Default: the provider's
|
|
717
|
+
* `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A node
|
|
718
|
+
* replaces it; `false` keeps the 0.46.0 heading, named for a screen reader only.
|
|
719
|
+
*/
|
|
720
|
+
header?: ReactNode | false;
|
|
706
721
|
}
|
|
707
722
|
|
|
708
723
|
/**
|
|
@@ -828,6 +843,7 @@ export function DataTable<T>({
|
|
|
828
843
|
edgeFade = false,
|
|
829
844
|
className,
|
|
830
845
|
rowActions,
|
|
846
|
+
rowActionsHeader,
|
|
831
847
|
toolbar,
|
|
832
848
|
footerLabel,
|
|
833
849
|
footer: footerProp,
|
|
@@ -860,6 +876,7 @@ export function DataTable<T>({
|
|
|
860
876
|
const { columns, swipesFromActions } = useRowActionColumn({
|
|
861
877
|
columns: columnsProp,
|
|
862
878
|
rowActions,
|
|
879
|
+
rowActionsHeader,
|
|
863
880
|
rowName,
|
|
864
881
|
labels,
|
|
865
882
|
density,
|
|
@@ -1196,6 +1213,17 @@ export function DataTable<T>({
|
|
|
1196
1213
|
const isMdUp = !usePhoneLayout();
|
|
1197
1214
|
// `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.
|
|
1198
1215
|
const desktopScroller = useRef<HTMLDivElement | null>(null);
|
|
1216
|
+
// 0.47 (keksdose S4): the scroller's visible width, as `--dt-visible-w` on it, for the
|
|
1217
|
+
// expansion row's pinned content (below). Set on the element, not in state: a resize
|
|
1218
|
+
// re-renders nothing. Only a table that can expand a row measures.
|
|
1219
|
+
const measuresVisibleWidth = isMdUp && !!expandedRow;
|
|
1220
|
+
useEffect(() => {
|
|
1221
|
+
const el = desktopScroller.current;
|
|
1222
|
+
if (!measuresVisibleWidth || !el || typeof ResizeObserver === "undefined") return;
|
|
1223
|
+
const ro = new ResizeObserver(() => el.style.setProperty("--dt-visible-w", `${el.clientWidth}px`));
|
|
1224
|
+
ro.observe(el);
|
|
1225
|
+
return () => ro.disconnect();
|
|
1226
|
+
}, [measuresVisibleWidth]);
|
|
1199
1227
|
const edgeFadeProps = useEdgeFade(desktopScroller, edgeFade && isMdUp);
|
|
1200
1228
|
|
|
1201
1229
|
// ---- Mobile card list (md:hidden) ----
|
|
@@ -1909,10 +1937,25 @@ export function DataTable<T>({
|
|
|
1909
1937
|
content in `w-0 min-w-full` pins the cell's preferred
|
|
1910
1938
|
width to ~0 (the explicit width:0 child), so it can never
|
|
1911
1939
|
perturb the columns; the div then fills to the full row
|
|
1912
|
-
width only at paint time, and long text wraps within it.
|
|
1940
|
+
width only at paint time, and long text wraps within it.
|
|
1941
|
+
Inside it the content is pinned to what the scroller SHOWS
|
|
1942
|
+
(0.47, keksdose S4): on a table wider than its scroller a
|
|
1943
|
+
table-wide panel put an inline editor's Save/Cancel off-screen.
|
|
1944
|
+
`sticky start-0` holds it at the scroller's start edge (the
|
|
1945
|
+
right one in RTL) as the row scrolls sideways, and
|
|
1946
|
+
`--dt-visible-w` (the scroller's clientWidth, measured above)
|
|
1947
|
+
sizes it; before the first measure, and in a table no wider
|
|
1948
|
+
than its scroller, it is the full width as before. */}
|
|
1913
1949
|
<td colSpan={totalColSpan} className="p-0">
|
|
1914
|
-
<div className=
|
|
1915
|
-
|
|
1950
|
+
<div className="w-0 min-w-full">
|
|
1951
|
+
<div
|
|
1952
|
+
className={cn(
|
|
1953
|
+
"sticky start-0 w-[var(--dt-visible-w,100%)] max-w-full",
|
|
1954
|
+
compact ? "px-2 py-2" : "px-3 py-3",
|
|
1955
|
+
)}
|
|
1956
|
+
>
|
|
1957
|
+
{expansion}
|
|
1958
|
+
</div>
|
|
1916
1959
|
</div>
|
|
1917
1960
|
</td>
|
|
1918
1961
|
</tr>
|
|
@@ -13,6 +13,7 @@ import { useCommitReason, type CommitScope } from "./write-lock";
|
|
|
13
13
|
import type { IconButtonGlyphSize, IconButtonSize } from "./icon-button";
|
|
14
14
|
import type { TooltipSide } from "./tooltip";
|
|
15
15
|
import type { DataTableColumn } from "./data-table";
|
|
16
|
+
import { TableHeaderCell, type TableHeaderCellProps } from "./table";
|
|
16
17
|
import { DEFAULT_ROW_ACTIONS_LABELS } from "./row-actions-labels";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -522,10 +523,57 @@ function InlineAction({
|
|
|
522
523
|
);
|
|
523
524
|
}
|
|
524
525
|
|
|
525
|
-
/** The actions column's
|
|
526
|
-
|
|
526
|
+
/** The actions column's heading, in the provider's words: on screen since 0.46.1 (keksdose
|
|
527
|
+
* dev#598 rework), a screen reader's name only with `hidden`. */
|
|
528
|
+
function RowActionsHeader({
|
|
529
|
+
text,
|
|
530
|
+
labels,
|
|
531
|
+
hidden,
|
|
532
|
+
}: {
|
|
533
|
+
text?: string;
|
|
534
|
+
labels?: LabelOverride<RowActionsLabels>;
|
|
535
|
+
hidden?: boolean;
|
|
536
|
+
}) {
|
|
527
537
|
const resolved = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS, labels);
|
|
528
|
-
|
|
538
|
+
const word = text ?? resolved.actions;
|
|
539
|
+
return hidden ? <span className="sr-only">{word}</span> : <>{word}</>;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** The actions cell's classes — `rowActionsColumn`'s `className`: the column hugs its
|
|
543
|
+
* buttons, on one line, at the row's end. A hand-built table's `<TableCell>` takes them
|
|
544
|
+
* around its {@link RowActions} (0.47, keksdose N17). */
|
|
545
|
+
export const ROW_ACTIONS_CELL_CLASS = "w-px whitespace-nowrap text-end";
|
|
546
|
+
/** The actions heading's classes — `rowActionsColumn`'s `headClassName`, which
|
|
547
|
+
* {@link RowActionsHead} draws: hugging, end-aligned over the buttons (0.47, N17). */
|
|
548
|
+
export const ROW_ACTIONS_HEAD_CLASS = "w-px text-end";
|
|
549
|
+
|
|
550
|
+
export interface RowActionsHeadProps extends Omit<TableHeaderCellProps, "children"> {
|
|
551
|
+
/** The heading. Default: the provider's `rowActions.actions` ("Actions"). */
|
|
552
|
+
children?: ReactNode;
|
|
553
|
+
/** Named for a screen reader only — `rowActionsColumn`'s `header: false`. */
|
|
554
|
+
hideLabel?: boolean;
|
|
555
|
+
labels?: LabelOverride<RowActionsLabels>;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* A hand-built table's actions heading (0.47, keksdose N17): the `<th>` `rowActionsColumn`
|
|
560
|
+
* gives a DataTable — "Actions" on screen in the provider's words, end-aligned over the
|
|
561
|
+
* buttons, hugging them. Pair it with `<TableCell className={ROW_ACTIONS_CELL_CLASS}>`
|
|
562
|
+
* around each row's {@link RowActions}. Every other `TableHeaderCell` prop passes on
|
|
563
|
+
* (`size`, the app's `className`, merged last).
|
|
564
|
+
*
|
|
565
|
+
* <RowActionsHead size="sm" className={TH} />
|
|
566
|
+
*/
|
|
567
|
+
export function RowActionsHead({ children, hideLabel, labels, className, ...rest }: RowActionsHeadProps) {
|
|
568
|
+
return (
|
|
569
|
+
<TableHeaderCell align="end" {...rest} className={cn(ROW_ACTIONS_HEAD_CLASS, className)}>
|
|
570
|
+
{typeof children === "string" || children == null ? (
|
|
571
|
+
<RowActionsHeader text={children ?? undefined} labels={labels} hidden={hideLabel} />
|
|
572
|
+
) : (
|
|
573
|
+
children
|
|
574
|
+
)}
|
|
575
|
+
</TableHeaderCell>
|
|
576
|
+
);
|
|
529
577
|
}
|
|
530
578
|
|
|
531
579
|
export interface RowActionsColumnOptions<T> {
|
|
@@ -546,10 +594,17 @@ export interface RowActionsColumnOptions<T> {
|
|
|
546
594
|
/**
|
|
547
595
|
* The column's name as plain text — the column-settings checklist and the phone
|
|
548
596
|
* card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left
|
|
549
|
-
* out, the
|
|
550
|
-
*
|
|
597
|
+
* out, the heading shows the provider's `rowActions.actions` and the plain-text
|
|
598
|
+
* places fall back to the column's `key`.
|
|
551
599
|
*/
|
|
552
600
|
headerText?: string;
|
|
601
|
+
/**
|
|
602
|
+
* The heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE "Actions" heading on
|
|
603
|
+
* every table with an actions column). Default: `headerText`, else the provider's
|
|
604
|
+
* `rowActions.actions` ("Actions"), shown, end-aligned over the buttons. A node
|
|
605
|
+
* replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader only.
|
|
606
|
+
*/
|
|
607
|
+
header?: ReactNode | false;
|
|
553
608
|
labels?: LabelOverride<RowActionsLabels>;
|
|
554
609
|
/** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */
|
|
555
610
|
column?: Partial<DataTableColumn<T>>;
|
|
@@ -561,8 +616,8 @@ export interface RowActionsColumnOptions<T> {
|
|
|
561
616
|
*
|
|
562
617
|
* columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}
|
|
563
618
|
*
|
|
564
|
-
* The
|
|
565
|
-
* content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the
|
|
619
|
+
* The heading says "Actions" on screen (0.46.1; `header` changes or hides it), the
|
|
620
|
+
* column hugs its content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the
|
|
566
621
|
* buttons keep their own clicks. On a phone it is a line of the card like any column;
|
|
567
622
|
* set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it
|
|
568
623
|
* then shows in the opened row instead).
|
|
@@ -576,12 +631,18 @@ export function rowActionsColumn<T>({
|
|
|
576
631
|
glyphSize,
|
|
577
632
|
tooltipSide,
|
|
578
633
|
headerText,
|
|
634
|
+
header,
|
|
579
635
|
labels,
|
|
580
636
|
column,
|
|
581
637
|
}: RowActionsColumnOptions<T>): DataTableColumn<T> {
|
|
582
638
|
return {
|
|
583
639
|
key,
|
|
584
|
-
header:
|
|
640
|
+
header:
|
|
641
|
+
header === false || header == null ? (
|
|
642
|
+
<RowActionsHeader text={headerText} labels={labels} hidden={header === false} />
|
|
643
|
+
) : (
|
|
644
|
+
header
|
|
645
|
+
),
|
|
585
646
|
headerText,
|
|
586
647
|
cell: (row) => (
|
|
587
648
|
<RowActions
|
|
@@ -594,8 +655,10 @@ export function rowActionsColumn<T>({
|
|
|
594
655
|
labels={labels}
|
|
595
656
|
/>
|
|
596
657
|
),
|
|
597
|
-
className:
|
|
598
|
-
|
|
658
|
+
className: ROW_ACTIONS_CELL_CLASS,
|
|
659
|
+
// The heading over the buttons, at the row's end (0.46.1); a hand-built table's
|
|
660
|
+
// `RowActionsHead` (0.47).
|
|
661
|
+
headClassName: ROW_ACTIONS_HEAD_CLASS,
|
|
599
662
|
noRowLink: true,
|
|
600
663
|
...column,
|
|
601
664
|
};
|
package/src/components/tabs.tsx
CHANGED
|
@@ -48,10 +48,16 @@ export interface TabsLabels {
|
|
|
48
48
|
add: string;
|
|
49
49
|
/** Accessible name of a tab's × — "Remove <tab>". */
|
|
50
50
|
remove: (tab: string) => string;
|
|
51
|
-
/** The `overflowCue`
|
|
51
|
+
/** The `overflowCue` › over the strip's end, while tabs lie past it: "More tabs". */
|
|
52
52
|
more: string;
|
|
53
|
-
/** The `overflowCue`
|
|
54
|
-
|
|
53
|
+
/** The `overflowCue` ‹ over the strip's start, while tabs lie before it: "Previous tabs". */
|
|
54
|
+
previous: string;
|
|
55
|
+
/**
|
|
56
|
+
* @deprecated 0.47: not drawn any more — the caret that turned into "back to the
|
|
57
|
+
* first tab" at the strip's end is now a ‹ of its own (`previous`). Accepted and
|
|
58
|
+
* ignored; removed in 0.48.
|
|
59
|
+
*/
|
|
60
|
+
backToFirst?: string;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
// Defined in ./ui-labels, which holds no component code (0.34, KS U8).
|
|
@@ -78,20 +84,22 @@ export interface TabsProps<T extends string>
|
|
|
78
84
|
onChange: (id: T) => void;
|
|
79
85
|
className?: string;
|
|
80
86
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
87
|
+
* Carets over the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose
|
|
88
|
+
* live #410 round 4): a › over the strip's end while tabs lie past it, and a ‹ over
|
|
89
|
+
* its start while tabs lie before it — each there only while something is hidden
|
|
90
|
+
* past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three
|
|
91
|
+
* quarters of the strip's width toward its own side. Neither ever changes direction:
|
|
92
|
+
* until 0.46 one caret turned round into "back to the first" at the end, and read as
|
|
93
|
+
* an arrow pointing the wrong way. Start and end follow the reading direction.
|
|
85
94
|
*
|
|
86
|
-
* The edge fade alone could read as the end of the row:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* `tabs.more` / `tabs.backToFirst`.
|
|
95
|
+
* The edge fade alone could read as the end of the row: the last visible tab can
|
|
96
|
+
* finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).
|
|
97
|
+
* The carets sit OVER the strip's ends (positioned, on the page's surface), not beside
|
|
98
|
+
* it, so the strip's width never changes as they come and go. Out of the tab order —
|
|
99
|
+
* the keyboard walks the strip with the arrow keys, which bring the focused tab into
|
|
100
|
+
* view. At every width. Ignored on a vertical strip (a side nav is a column, not a
|
|
101
|
+
* row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is
|
|
102
|
+
* the reference this replaces.
|
|
95
103
|
*/
|
|
96
104
|
overflowCue?: boolean;
|
|
97
105
|
/**
|
|
@@ -254,36 +262,43 @@ export function Tabs<T extends string>({
|
|
|
254
262
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
255
263
|
const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
|
|
256
264
|
const fade = useStripFade(stripRef);
|
|
257
|
-
// See `overflowCue
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
265
|
+
// See `overflowCue`: a ‹ over the start while tabs lie before it, a › over the end
|
|
266
|
+
// while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.
|
|
267
|
+
const cue = overflowCue && !vertical;
|
|
268
|
+
const cueStart = cue && (fade.overflow === "start" || fade.overflow === "both");
|
|
269
|
+
const cueEnd = cue && (fade.overflow === "end" || fade.overflow === "both");
|
|
270
|
+
// Three quarters of the visible width toward `side`; an RTL strip runs leftward.
|
|
271
|
+
const onCue = (side: "start" | "end") => {
|
|
261
272
|
const strip = stripRef.current;
|
|
262
273
|
if (!strip) return;
|
|
263
274
|
const behavior: ScrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
264
|
-
if (cueAtEnd) {
|
|
265
|
-
strip.scrollTo({ left: 0, behavior });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
275
|
const rtl = getComputedStyle(strip).direction === "rtl";
|
|
269
|
-
|
|
276
|
+
const toward = (side === "end" ? 1 : -1) * (rtl ? -1 : 1);
|
|
277
|
+
strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });
|
|
270
278
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
279
|
+
// OVER the strip's end, on the page's surface, so the caret reads as a control and not
|
|
280
|
+
// as one more faded tab — and so it can come and go without the strip changing width
|
|
281
|
+
// under the thumb. The position sits on a wrapper of ours: IconButton puts its
|
|
282
|
+
// tooltip's span around the button, and a class on the button would land inside it.
|
|
283
|
+
const cueAt = (side: "start" | "end") => (
|
|
284
|
+
<div
|
|
285
|
+
data-tabs-overflow-cue={side}
|
|
286
|
+
className={cn("absolute top-1/2 z-10 flex -translate-y-1/2", side === "start" ? "start-0" : "end-0")}
|
|
287
|
+
>
|
|
274
288
|
<IconButton
|
|
275
289
|
size="sm"
|
|
276
|
-
variant="
|
|
277
|
-
|
|
290
|
+
variant="secondary"
|
|
291
|
+
className="bg-surface shadow-sm"
|
|
278
292
|
tabIndex={-1}
|
|
279
293
|
type="button"
|
|
280
|
-
|
|
281
|
-
|
|
294
|
+
label={side === "start" ? text.previous : text.more}
|
|
295
|
+
labelVisible={false}
|
|
296
|
+
onClick={() => onCue(side)}
|
|
282
297
|
>
|
|
283
|
-
{
|
|
298
|
+
{side === "start" ? <ChevronLeft className="rtl:-scale-x-100" /> : <ChevronRight className="rtl:-scale-x-100" />}
|
|
284
299
|
</IconButton>
|
|
285
300
|
</div>
|
|
286
|
-
)
|
|
301
|
+
);
|
|
287
302
|
// The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a
|
|
288
303
|
// deep link to the seventh report. A selection behind the cut edge was the worst
|
|
289
304
|
// case of the overflow above: the one tab the panel below belongs to, sliced to
|
|
@@ -407,7 +422,7 @@ export function Tabs<T extends string>({
|
|
|
407
422
|
// — it is the box a caller's margin or width is meant for.
|
|
408
423
|
className={cn(
|
|
409
424
|
vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,
|
|
410
|
-
|
|
425
|
+
cue ? "min-w-0 flex-1" : onAdd ? "min-w-0" : className,
|
|
411
426
|
)}
|
|
412
427
|
>
|
|
413
428
|
{tabs.map((tab) => {
|
|
@@ -575,16 +590,24 @@ export function Tabs<T extends string>({
|
|
|
575
590
|
{text.remove(nameOf(activeTab))}
|
|
576
591
|
</button>
|
|
577
592
|
) : null;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
593
|
+
// The positioned box the carets sit in, over the strip's two ends (see `overflowCue`).
|
|
594
|
+
// There whether or not the strip overflows right now, so a resize never re-parents it.
|
|
595
|
+
const stripBox = cue ? (
|
|
596
|
+
<div className={cn("relative flex min-w-0", !onAdd && "flex-1")}>
|
|
597
|
+
{tablist}
|
|
598
|
+
{cueStart && cueAt("start")}
|
|
599
|
+
{cueEnd && cueAt("end")}
|
|
600
|
+
</div>
|
|
601
|
+
) : (
|
|
602
|
+
tablist
|
|
603
|
+
);
|
|
604
|
+
if (!onAdd && cue) {
|
|
605
|
+
// `className` goes on the row around the strip — it is the box a caller's margin or
|
|
606
|
+
// width is meant for, as with an add button — and stays there at every scroll position.
|
|
583
607
|
return (
|
|
584
608
|
<div className={cn("flex items-end", className)}>
|
|
585
|
-
{
|
|
609
|
+
{stripBox}
|
|
586
610
|
{removeButton}
|
|
587
|
-
{cueButton}
|
|
588
611
|
</div>
|
|
589
612
|
);
|
|
590
613
|
}
|
|
@@ -614,9 +637,8 @@ export function Tabs<T extends string>({
|
|
|
614
637
|
}
|
|
615
638
|
return (
|
|
616
639
|
<div className={cn("flex items-end", className)}>
|
|
617
|
-
{
|
|
640
|
+
{stripBox}
|
|
618
641
|
{removeButton}
|
|
619
|
-
{cueButton}
|
|
620
642
|
{/* Outside the tablist: an action, not a tab — and a tablist may own only
|
|
621
643
|
tabs. The rule under the strip carries on beneath it. */}
|
|
622
644
|
<div
|
|
@@ -838,7 +838,9 @@ export function TranslationReviewPanel({
|
|
|
838
838
|
} else if (editable) {
|
|
839
839
|
columns.push({
|
|
840
840
|
key: "approve",
|
|
841
|
-
|
|
841
|
+
// On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).
|
|
842
|
+
header: rowActionLabels.actions,
|
|
843
|
+
headerText: rowActionLabels.actions,
|
|
842
844
|
cell: (r) =>
|
|
843
845
|
r.status === "approved" || r.text === "" ? null : (
|
|
844
846
|
<IconButton
|
package/src/data-table.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type {
|
|
|
27
27
|
DataTableRowAction,
|
|
28
28
|
DataTableDesktopSwipeActions,
|
|
29
29
|
} from "./components/data-table";
|
|
30
|
+
// 0.46.1 (keksdose N3): a swipe plan as `RowActions`' actions — the desktop column's own
|
|
31
|
+
// derivation, for an app's hand-built actions column.
|
|
32
|
+
export { swipePlanRowActions } from "./components/data-table-row-actions";
|
|
30
33
|
export * from "./components/data-table-cells";
|
|
31
34
|
export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
|
|
32
35
|
export type {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { MouseEvent } from "react";
|
|
3
|
+
import { Download, FileText } from "lucide-react";
|
|
4
|
+
import { Button } from "../components/button";
|
|
5
|
+
import { toast } from "../components/toast";
|
|
6
|
+
import type { AuthedFetcher } from "../hooks/use-authed-src";
|
|
7
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
import { filenameFromDisposition, saveBlob } from "../lib/download";
|
|
9
|
+
import { attachmentName } from "./feedback-record";
|
|
10
|
+
import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels";
|
|
11
|
+
|
|
12
|
+
// Internal (not in the `./feedback` entry): the row detail's download button and its
|
|
13
|
+
// plain fetch, shared since 0.47 with ./feedback-entry-file's inline entry files.
|
|
14
|
+
|
|
15
|
+
/** The bytes behind a fetcher's answer — a `Blob` as it is, a `Response` checked. */
|
|
16
|
+
async function toBlob(answer: Blob | Response): Promise<Blob> {
|
|
17
|
+
if (typeof Response !== "undefined" && answer instanceof Response) {
|
|
18
|
+
if (!answer.ok) throw new Error(`HTTP ${answer.status}`);
|
|
19
|
+
return answer.blob();
|
|
20
|
+
}
|
|
21
|
+
return answer as Blob;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** No fetcher: the browser's own `fetch`, with the cookies a same-origin app uses. */
|
|
25
|
+
export const plainFeedbackFetch: AuthedFetcher = (url, { signal }) =>
|
|
26
|
+
fetch(url, { signal, credentials: "same-origin" });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A PDF or a text log (keksdose `FeedbackFileDownload`, dev #578): the bytes sit behind
|
|
30
|
+
* an authenticated route that serves them as `attachment`, never inline (§3.5), so a
|
|
31
|
+
* plain link would be a 401 the moment it was opened. Fetched through the app's
|
|
32
|
+
* fetcher and saved with {@link saveBlob}; a failure says so in a toast.
|
|
33
|
+
*
|
|
34
|
+
* Saved under the name the server gives it when the fetcher answers with a `Response`
|
|
35
|
+
* (server-kit's `content_disposition` puts the uploader's own name in `filename*`), and
|
|
36
|
+
* under the URL's last segment — the name on the button — when it answers with a bare
|
|
37
|
+
* `Blob`, which has no headers to ask. Words: `feedbackDetail.download` /
|
|
38
|
+
* `downloadFailed`.
|
|
39
|
+
*/
|
|
40
|
+
export function FeedbackAttachmentDownload({ url, fetcher }: { url: string; fetcher: AuthedFetcher }) {
|
|
41
|
+
const labels = useKitLabels("feedbackDetail", DEFAULT_FEEDBACK_DETAIL_LABELS);
|
|
42
|
+
const [busy, setBusy] = useState(false);
|
|
43
|
+
const name = attachmentName(url);
|
|
44
|
+
const download = async (event: MouseEvent) => {
|
|
45
|
+
// The row around the detail may toggle on a click.
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
setBusy(true);
|
|
48
|
+
try {
|
|
49
|
+
const answer = await fetcher(url, { signal: new AbortController().signal });
|
|
50
|
+
const blob = await toBlob(answer);
|
|
51
|
+
const saveAs =
|
|
52
|
+
typeof Response !== "undefined" && answer instanceof Response
|
|
53
|
+
? filenameFromDisposition(answer.headers.get("Content-Disposition"), name)
|
|
54
|
+
: name;
|
|
55
|
+
saveBlob(blob, saveAs);
|
|
56
|
+
} catch {
|
|
57
|
+
toast.error(labels.downloadFailed);
|
|
58
|
+
} finally {
|
|
59
|
+
setBusy(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return (
|
|
63
|
+
<Button
|
|
64
|
+
type="button"
|
|
65
|
+
variant="secondary"
|
|
66
|
+
size="sm"
|
|
67
|
+
pending={busy}
|
|
68
|
+
onClick={(event) => void download(event)}
|
|
69
|
+
aria-label={labels.download(name)}
|
|
70
|
+
className="max-w-full"
|
|
71
|
+
>
|
|
72
|
+
<FileText className="size-3.5 shrink-0" aria-hidden />
|
|
73
|
+
<span className="min-w-0 max-w-[14rem] truncate-until-large">{name}</span>
|
|
74
|
+
<Download className="size-3.5 shrink-0" aria-hidden />
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -4,11 +4,13 @@ import { EyeOff } from "lucide-react";
|
|
|
4
4
|
import { Button } from "../components/button";
|
|
5
5
|
import { useConfirm } from "../components/confirm-dialog";
|
|
6
6
|
import type { CommitScope } from "../components/write-lock";
|
|
7
|
+
import type { AuthedFetcher } from "../hooks/use-authed-src";
|
|
7
8
|
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
8
9
|
import { FeedbackStatusBadge } from "./feedback-inbox";
|
|
9
10
|
import type { FeedbackComment, FeedbackReply } from "./feedback-public-types";
|
|
10
11
|
import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
|
|
11
12
|
import { FEEDBACK_COMMENT_MAX, attachmentName } from "./feedback-record";
|
|
13
|
+
import { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from "./feedback-entry-file";
|
|
12
14
|
import { useFeedbackRepliesLabels } from "./feedback-replies";
|
|
13
15
|
import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context";
|
|
14
16
|
import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip";
|
|
@@ -34,8 +36,13 @@ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels
|
|
|
34
36
|
* sees their text with that line; the admin sees it struck through, with **Unhide**.
|
|
35
37
|
* An admin has **Hide** on every visible comment; the writer **Delete** on their own,
|
|
36
38
|
* after a confirm (it is a hard delete). Nobody edits a comment.
|
|
37
|
-
* - **The composer** (1–4 000 characters
|
|
38
|
-
*
|
|
39
|
+
* - **The composer** (1–4 000 characters) only with `onComment`: left out for a reader
|
|
40
|
+
* who may not write (a demo, a CRASH report — the app knows). Since 0.47 (§12) with
|
|
41
|
+
* `onUpload` it takes one file as the rework note does — pasted into the box,
|
|
42
|
+
* captured or picked — uploaded first, its URL `onComment`'s second argument.
|
|
43
|
+
* - **Files** (0.47, §12): a comment's or an entry's file under its text, a picture
|
|
44
|
+
* INLINE — a thumbnail through `fetcher` that opens full size — any other file a
|
|
45
|
+
* download ({@link FeedbackEntryFile}). A veiled comment shows none.
|
|
39
46
|
*
|
|
40
47
|
* Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the
|
|
41
48
|
* viewer's needs the app's `ConfirmProvider` (each app mounts one already).
|
|
@@ -75,7 +82,7 @@ export { DEFAULT_FEEDBACK_COMMENTS_LABELS };
|
|
|
75
82
|
|
|
76
83
|
/* ── The thread ───────────────────────────────────────────────────────────── */
|
|
77
84
|
|
|
78
|
-
export interface FeedbackCommentThreadProps {
|
|
85
|
+
export interface FeedbackCommentThreadProps extends FeedbackEntryUploadOptions {
|
|
79
86
|
/** The report's replies (§9.7), oldest first. Left out or `[]`: none.
|
|
80
87
|
*
|
|
81
88
|
* Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's
|
|
@@ -92,8 +99,12 @@ export interface FeedbackCommentThreadProps {
|
|
|
92
99
|
/** `POST /feedback/{id}/comments`. Return the promise: the box clears when it
|
|
93
100
|
* resolves, and keeps the draft when it rejects. A failure is the app's to toast —
|
|
94
101
|
* the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no
|
|
95
|
-
* composer.
|
|
96
|
-
|
|
102
|
+
* composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with
|
|
103
|
+
* `onUpload`), `undefined` for none — `attachment_url` in the body (server-kit 0.18). */
|
|
104
|
+
onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;
|
|
105
|
+
/** 0.47: the app's authenticated GET (`AuthedImage`'s) for the files in the thread.
|
|
106
|
+
* Left out, the browser's own fetch with same-origin cookies. */
|
|
107
|
+
fetcher?: AuthedFetcher | null;
|
|
97
108
|
/** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection
|
|
98
109
|
* is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left
|
|
99
110
|
* out, no Hide / Unhide. */
|
|
@@ -141,9 +152,18 @@ export function FeedbackCommentThread({
|
|
|
141
152
|
loading = false,
|
|
142
153
|
commit,
|
|
143
154
|
now,
|
|
155
|
+
fetcher,
|
|
156
|
+
onUpload,
|
|
157
|
+
onCaptureScreenshot,
|
|
158
|
+
attachment,
|
|
144
159
|
labels: labelsProp,
|
|
145
160
|
className,
|
|
146
161
|
}: FeedbackCommentThreadProps) {
|
|
162
|
+
const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({
|
|
163
|
+
onUpload,
|
|
164
|
+
onCaptureScreenshot,
|
|
165
|
+
attachment,
|
|
166
|
+
});
|
|
147
167
|
const labels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);
|
|
148
168
|
const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
|
|
149
169
|
const replyLabels = useFeedbackRepliesLabels();
|
|
@@ -189,6 +209,10 @@ export function FeedbackCommentThread({
|
|
|
189
209
|
author: reply.author_name ?? replyLabels.admin,
|
|
190
210
|
createdAt: reply.created_at,
|
|
191
211
|
body: reply.body,
|
|
212
|
+
// 0.47 (§12): an admin reply's file, where the view carries it.
|
|
213
|
+
attachments: reply.attachment_url
|
|
214
|
+
? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]
|
|
215
|
+
: undefined,
|
|
192
216
|
staff: true,
|
|
193
217
|
badge: reply.status_to ? (
|
|
194
218
|
<span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
|
|
@@ -244,6 +268,11 @@ export function FeedbackCommentThread({
|
|
|
244
268
|
author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),
|
|
245
269
|
createdAt: comment.created_at,
|
|
246
270
|
body: veiled ? "" : (comment.body ?? ""),
|
|
271
|
+
// 0.47 (§12): its one file; none when veiled (the server sends none either).
|
|
272
|
+
attachments:
|
|
273
|
+
!veiled && comment.attachment_url
|
|
274
|
+
? [{ name: attachmentName(comment.attachment_url), url: comment.attachment_url }]
|
|
275
|
+
: undefined,
|
|
247
276
|
staff: !veiled && comment.staff,
|
|
248
277
|
struck: comment.hidden && isAdmin && !veiled,
|
|
249
278
|
footer: actions.length > 0 ? actions : undefined,
|
|
@@ -258,13 +287,19 @@ export function FeedbackCommentThread({
|
|
|
258
287
|
messages={entries.map((entry) => entry.message)}
|
|
259
288
|
loading={loading}
|
|
260
289
|
now={now}
|
|
290
|
+
renderAttachment={(file) => (file.url ? <FeedbackEntryFile url={file.url} fetcher={fetcher} /> : file.name)}
|
|
261
291
|
labels={{ thread: labels.comments, empty: labels.empty }}
|
|
262
292
|
/>
|
|
263
293
|
{onComment && (
|
|
264
294
|
<ChatComposer
|
|
265
295
|
className="mt-3"
|
|
266
|
-
onSend={(body) =>
|
|
267
|
-
|
|
296
|
+
onSend={async (body, file) => {
|
|
297
|
+
const url = await upload(file);
|
|
298
|
+
// The second argument only with a file, so a text comment calls as before 0.47.
|
|
299
|
+
return url === undefined ? onComment(body) : onComment(body, url);
|
|
300
|
+
}}
|
|
301
|
+
pending={pending || uploading}
|
|
302
|
+
attachment={composerAttachment}
|
|
268
303
|
commit={commit}
|
|
269
304
|
maxLength={FEEDBACK_COMMENT_MAX}
|
|
270
305
|
showCount
|