@drivenets/design-system 0.12.1 → 0.13.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/CHANGELOG.md +15 -0
- package/dist/index.cjs +1097 -308
- package/dist/index.d.cts +1118 -982
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1118 -982
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1089 -309
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +31 -31
- package/src/components/ds-date-input/ds-date-input.utils.ts +2 -2
- package/src/components/ds-drawer/ds-drawer.tsx +2 -0
- package/src/components/ds-form-control/ds-form-control.tsx +14 -11
- package/src/components/ds-form-control/ds-form-control.types.ts +4 -0
- package/src/components/ds-grid/ds-grid.tsx +1 -2
- package/src/components/ds-modal/ds-modal.tsx +3 -0
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +21 -2
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +14 -11
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +1 -6
- package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +2 -2
- package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +2 -0
- package/src/components/ds-table/components/ds-table-cell/ds-table-cell.tsx +42 -11
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +24 -4
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +22 -1
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +22 -0
- package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +1 -0
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +19 -0
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -1
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +1 -1
- package/src/components/ds-table/components/edit/cell-editors/cell-editors.module.scss +3 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +27 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +50 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +51 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +36 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +50 -0
- package/src/components/ds-table/components/edit/cell-editors/index.ts +14 -0
- package/src/components/ds-table/components/edit/cell-editors/table-edit-form-control.tsx +31 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +28 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +39 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.types.ts +7 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/index.ts +2 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.module.scss +48 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +76 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.types.ts +5 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/index.ts +2 -0
- package/src/components/ds-table/context/ds-table-context.ts +79 -1
- package/src/components/ds-table/ds-table-column.types.ts +12 -0
- package/src/components/ds-table/ds-table.module.scss +100 -10
- package/src/components/ds-table/ds-table.tsx +18 -19
- package/src/components/ds-table/ds-table.types.ts +52 -0
- package/src/components/ds-table/filters/adapters/create-checkbox-filter-adapter.tsx +24 -40
- package/src/components/ds-table/filters/adapters/create-dual-range-filter-adapter.tsx +10 -31
- package/src/components/ds-table/filters/adapters/create-filter-adapter.ts +6 -98
- package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.module.scss +36 -0
- package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.tsx +92 -13
- package/src/components/ds-table/filters/components/checkbox-filter/index.ts +1 -1
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.module.scss +42 -0
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx +89 -0
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.types.ts +25 -0
- package/src/components/ds-table/filters/components/column-filter-popover/index.ts +5 -0
- package/src/components/ds-table/filters/hooks/use-table-filters.ts +113 -78
- package/src/components/ds-table/filters/index.ts +2 -44
- package/src/components/ds-table/filters/types/column-meta.augmentation.ts +19 -0
- package/src/components/ds-table/filters/types/filter-adapter.types.ts +48 -81
- package/src/components/ds-table/hooks/use-cell-editor.ts +81 -0
- package/src/components/ds-table/index.ts +29 -0
- package/src/components/ds-table/utils/column-size.ts +9 -8
- package/src/components/ds-tag/ds-tag.tsx +2 -2
- package/src/components/ds-workspace-layout/ds-workspace-layout.context.ts +19 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +166 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +135 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.types.ts +52 -0
- package/src/components/ds-workspace-layout/index.ts +11 -0
- package/src/index.ts +1 -1
- package/src/styles/_scrollbars.scss +37 -0
- package/src/utils/restore-document-body-modal-styles.ts +44 -0
- package/src/utils/use-ark-dialog-body-lock-cleanup.ts +36 -0
- package/src/components/ds-workspace/ds-workspace.module.scss +0 -55
- package/src/components/ds-workspace/ds-workspace.tsx +0 -37
- package/src/components/ds-workspace/ds-workspace.types.ts +0 -17
- package/src/components/ds-workspace/index.ts +0 -8
|
@@ -1,117 +1,84 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { Row } from '@tanstack/react-table';
|
|
3
|
-
import type {
|
|
3
|
+
import type { TagFilterItem } from '../../../ds-tag-filter';
|
|
4
4
|
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
6
|
export type AnyAdapter = FilterAdapter<any, any, any>;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @template TData - The row data type
|
|
13
|
-
* @template TFilterValue - The filter state value type
|
|
14
|
-
* @template TCellValue - The actual cell value type from the table (defaults to unknown)
|
|
9
|
+
* Contract every filter implements. `TData` is the row type, `TFilterValue`
|
|
10
|
+
* the adapter's state, `TCellValue` the cell value returned by the column.
|
|
15
11
|
*/
|
|
16
12
|
export interface FilterAdapter<TData, TFilterValue, TCellValue = unknown> {
|
|
17
|
-
/**
|
|
18
|
-
* Unique identifier for the filter (should match column accessorKey)
|
|
19
|
-
*/
|
|
13
|
+
/** Unique id, should match the column `accessorKey`. */
|
|
20
14
|
id: string;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Display label for the filter in the navigation
|
|
24
|
-
*/
|
|
15
|
+
/** Display label used in the filter navigation. */
|
|
25
16
|
label: string;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Initial/default value for the filter state
|
|
29
|
-
*/
|
|
17
|
+
/** Initial / reset value. */
|
|
30
18
|
initialValue: TFilterValue;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* TanStack Table filter function
|
|
34
|
-
* Determines if a row matches the current filter value
|
|
35
|
-
*/
|
|
19
|
+
/** TanStack `filterFn` — returns whether a row matches the current value. */
|
|
36
20
|
columnFilterFn: (row: Row<TData>, columnId: string, filterValue: TFilterValue) => boolean;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Optional custom cell renderer for the table column
|
|
40
|
-
* Receives the actual cell value from the table, not the filter value
|
|
41
|
-
* If not provided, default rendering will be used
|
|
42
|
-
*/
|
|
21
|
+
/** Optional cell renderer; receives the cell value, not the filter value. */
|
|
43
22
|
cellRenderer?: (value: TCellValue) => ReactNode;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Convert filter value to filter chips for display
|
|
47
|
-
* Returns empty array if no chips should be shown
|
|
48
|
-
*/
|
|
49
|
-
toChips: (value: TFilterValue) => ChipItem[];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Remove a chip from the filter value
|
|
53
|
-
* Returns updated filter value with the chip's effect removed
|
|
54
|
-
*/
|
|
55
|
-
fromChip: (chip: ChipItem, currentValue: TFilterValue) => TFilterValue;
|
|
56
|
-
|
|
57
23
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Can also be used to check if filter is active (count > 0)
|
|
24
|
+
* Convert the filter value into chips for `DsTagFilter`. Each chip's
|
|
25
|
+
* `metadata.key` / `metadata.value` must let `fromChip` undo it.
|
|
61
26
|
*/
|
|
27
|
+
toChips: (value: TFilterValue) => TagFilterItem[];
|
|
28
|
+
/** Remove a chip's effect from the current value. */
|
|
29
|
+
fromChip: (chip: TagFilterItem, currentValue: TFilterValue) => TFilterValue;
|
|
30
|
+
/** Number of active sub-filters; `0` means the filter is inactive. */
|
|
62
31
|
getActiveFiltersCount: (value: TFilterValue) => number;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Reset filter to initial state
|
|
66
|
-
*/
|
|
32
|
+
/** Return the initial value. */
|
|
67
33
|
reset: () => TFilterValue;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Render the filter UI component
|
|
71
|
-
* Receives current value and onChange callback
|
|
72
|
-
*/
|
|
34
|
+
/** Render the filter UI for a given value + onChange. */
|
|
73
35
|
renderFilter: (value: TFilterValue, onChange: (value: TFilterValue) => void) => ReactNode;
|
|
74
36
|
}
|
|
75
37
|
|
|
76
|
-
/**
|
|
77
|
-
* Filter navigation item used to render filter tabs in the UI
|
|
78
|
-
*/
|
|
79
38
|
export interface FilterNavItem {
|
|
80
|
-
/**
|
|
81
|
-
* Unique identifier for the filter (matches FilterAdapter.id)
|
|
82
|
-
*/
|
|
39
|
+
/** Matches `FilterAdapter.id`. */
|
|
83
40
|
id: string;
|
|
84
|
-
/**
|
|
85
|
-
* Display label for the filter tab
|
|
86
|
-
*/
|
|
87
41
|
label: string;
|
|
88
|
-
/**
|
|
89
|
-
* Optional count badge to display active filters
|
|
90
|
-
*/
|
|
42
|
+
/** Active filter count badge. */
|
|
91
43
|
count?: number;
|
|
92
|
-
/**
|
|
93
|
-
* Whether this filter is disabled
|
|
94
|
-
*/
|
|
95
44
|
disabled?: boolean;
|
|
96
45
|
}
|
|
97
46
|
|
|
98
|
-
/**
|
|
99
|
-
* Filter state managed by useTableFilters hook
|
|
100
|
-
*/
|
|
47
|
+
/** Map of filter id → adapter value. */
|
|
101
48
|
export interface FilterState<TValue> {
|
|
102
49
|
[filterId: string]: TValue;
|
|
103
50
|
}
|
|
104
51
|
|
|
105
|
-
/**
|
|
106
|
-
* Column filter for TanStack Table
|
|
107
|
-
*/
|
|
108
52
|
export interface ColumnFilterState<TValue> {
|
|
109
|
-
/**
|
|
110
|
-
* Column identifier this filter applies to (matches the column `accessorKey`).
|
|
111
|
-
*/
|
|
53
|
+
/** Column id (matches the column `accessorKey`). */
|
|
112
54
|
id: string;
|
|
113
|
-
/**
|
|
114
|
-
* Current filter value, passed to the column's filter function.
|
|
115
|
-
*/
|
|
55
|
+
/** Value passed to the column's filter function. */
|
|
116
56
|
value: TValue;
|
|
117
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set by the consumer on `column.meta.filter` to opt a column into the
|
|
61
|
+
* per-column filter popover. Provide an `adapter` inline or just `adapterId`
|
|
62
|
+
* to resolve against `useTableFilters({ filterAdapters })`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ColumnFilterMeta {
|
|
65
|
+
/** Mutually exclusive with `adapterId`. */
|
|
66
|
+
adapter?: AnyAdapter;
|
|
67
|
+
/** Mutually exclusive with `adapter`. */
|
|
68
|
+
adapterId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What `useTableFilters` injects onto `column.meta.filter` of each enhanced
|
|
73
|
+
* column. Consumers don't build this — they set `ColumnFilterMeta` and the
|
|
74
|
+
* hook swaps it for this resolved shape on `enhancedColumns`.
|
|
75
|
+
*/
|
|
76
|
+
export interface ResolvedColumnFilter {
|
|
77
|
+
filterId: string;
|
|
78
|
+
hasActiveFilter: boolean;
|
|
79
|
+
/** Renders the adapter's filter UI for this column. */
|
|
80
|
+
render: () => ReactNode;
|
|
81
|
+
onApply: () => void;
|
|
82
|
+
onClear: () => void;
|
|
83
|
+
onCancel: () => void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type KeyboardEventHandler, type MouseEvent, type MouseEventHandler } from 'react';
|
|
2
|
+
import type { CellContext, RowData } from '@tanstack/react-table';
|
|
3
|
+
import { useDsTableContext } from '../context/ds-table-context';
|
|
4
|
+
|
|
5
|
+
export interface UseCellEditorOptions<TData extends RowData, TValue> {
|
|
6
|
+
/**
|
|
7
|
+
* The TanStack Table CellContext for this cell. Pass through
|
|
8
|
+
* the argument received in `ColumnDef.editCell`.
|
|
9
|
+
*/
|
|
10
|
+
cellContext: CellContext<TData, TValue>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseCellEditorContainerProps {
|
|
14
|
+
onKeyDown: KeyboardEventHandler<HTMLElement>;
|
|
15
|
+
onClick: MouseEventHandler;
|
|
16
|
+
onDoubleClick: MouseEventHandler;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseCellEditorResult<TValue> {
|
|
20
|
+
/** The in-progress draft value for the active editor. */
|
|
21
|
+
value: TValue;
|
|
22
|
+
/** Validation error from the most recent commit attempt, if any. */
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Update the in-progress draft value. */
|
|
25
|
+
setValue: (value: TValue) => void;
|
|
26
|
+
/** Validate and commit the draft. */
|
|
27
|
+
commit: (overrideValue?: TValue) => void;
|
|
28
|
+
/** Discard the draft and exit edit mode. */
|
|
29
|
+
cancel: () => void;
|
|
30
|
+
/** Stop mouse events from triggering row-level handlers. */
|
|
31
|
+
stopPropagation: (event: MouseEvent) => void;
|
|
32
|
+
/** Spread on the custom `editCell` root for pointer isolation from row handlers. */
|
|
33
|
+
editorContainerProps: UseCellEditorContainerProps;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wires a TanStack Table `editCell` renderer into DsTable's hoisted editing state.
|
|
38
|
+
* Only mount this hook inside `ColumnDef.editCell` — the table cell layer handles
|
|
39
|
+
* entering edit mode on double-click.
|
|
40
|
+
*/
|
|
41
|
+
export const useCellEditor = <TData extends RowData, TValue>({
|
|
42
|
+
cellContext,
|
|
43
|
+
}: UseCellEditorOptions<TData, TValue>): UseCellEditorResult<TValue> => {
|
|
44
|
+
const ctx = useDsTableContext<TData, TValue>();
|
|
45
|
+
const { row, column, getValue } = cellContext;
|
|
46
|
+
|
|
47
|
+
const isActiveCell =
|
|
48
|
+
ctx.editing !== null && ctx.editing.cell.row.id === row.id && ctx.editing.cell.column.id === column.id;
|
|
49
|
+
const value = isActiveCell && ctx.editing ? ctx.editing.draftValue : getValue();
|
|
50
|
+
const error = isActiveCell && ctx.editing ? ctx.editing.error : null;
|
|
51
|
+
|
|
52
|
+
const setValue = (next: TValue) => {
|
|
53
|
+
ctx.setDraft(next);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const commit = (overrideValue?: TValue) => {
|
|
57
|
+
ctx.commit(overrideValue);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const cancel = () => {
|
|
61
|
+
ctx.cancel();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const stopPropagation = (event: MouseEvent) => {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
value,
|
|
70
|
+
error,
|
|
71
|
+
setValue,
|
|
72
|
+
commit,
|
|
73
|
+
cancel,
|
|
74
|
+
stopPropagation,
|
|
75
|
+
editorContainerProps: {
|
|
76
|
+
onKeyDown: () => {},
|
|
77
|
+
onClick: stopPropagation,
|
|
78
|
+
onDoubleClick: stopPropagation,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -5,3 +5,32 @@ export * from './filters';
|
|
|
5
5
|
|
|
6
6
|
export { default as DsTable } from './ds-table';
|
|
7
7
|
export * from './ds-table.types';
|
|
8
|
+
|
|
9
|
+
export { useCellEditor } from './hooks/use-cell-editor';
|
|
10
|
+
export type {
|
|
11
|
+
UseCellEditorOptions,
|
|
12
|
+
UseCellEditorResult,
|
|
13
|
+
UseCellEditorContainerProps,
|
|
14
|
+
} from './hooks/use-cell-editor';
|
|
15
|
+
|
|
16
|
+
export { DsTableEditableCell } from './components/edit/ds-table-editable-cell';
|
|
17
|
+
export type { DsTableEditableCellProps } from './components/edit/ds-table-editable-cell';
|
|
18
|
+
|
|
19
|
+
export { DsTableEditingCell } from './components/edit/ds-table-editing-cell';
|
|
20
|
+
export type { DsTableEditingCellProps } from './components/edit/ds-table-editing-cell';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
DsTableEditCellText,
|
|
24
|
+
DsTableEditCellNumber,
|
|
25
|
+
DsTableEditCellSelect,
|
|
26
|
+
DsTableEditCellDate,
|
|
27
|
+
DsTableEditCellCheckbox,
|
|
28
|
+
} from './components/edit/cell-editors';
|
|
29
|
+
export type {
|
|
30
|
+
DsTableEditCellTextProps,
|
|
31
|
+
DsTableEditCellNumberProps,
|
|
32
|
+
DsTableEditCellSelectProps,
|
|
33
|
+
DsTableEditCellDateProps,
|
|
34
|
+
DsTableEditCellDateValue,
|
|
35
|
+
DsTableEditCellCheckboxProps,
|
|
36
|
+
} from './components/edit/cell-editors';
|
|
@@ -2,19 +2,20 @@ import type { CSSProperties } from 'react';
|
|
|
2
2
|
import { defaultColumnSizing } from '@tanstack/react-table';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates the appropriate style object for a table column/cell based on its size
|
|
5
|
+
* Generates the appropriate style object for a table column/cell based on its size
|
|
6
|
+
*
|
|
7
|
+
* Custom-sized columns get a fixed `width` plus `minWidth` and `flexShrink: 0` so
|
|
8
|
+
* they enforce horizontal overflow on the table container. Default-sized columns
|
|
9
|
+
* grow to fill the row evenly via `flex: 1`.
|
|
6
10
|
*
|
|
7
11
|
* @param columnSize - The size of the column
|
|
8
|
-
* @
|
|
9
|
-
* @returns Style object or undefined if no custom styling is needed
|
|
12
|
+
* @returns Style object for the column/cell
|
|
10
13
|
*/
|
|
11
|
-
export const getColumnSizeStyle = (columnSize: number
|
|
14
|
+
export const getColumnSizeStyle = (columnSize: number): CSSProperties => {
|
|
12
15
|
const hasCustomSize = columnSize !== defaultColumnSizing.size;
|
|
13
16
|
if (hasCustomSize) {
|
|
14
|
-
return { width: columnSize };
|
|
17
|
+
return { width: columnSize, minWidth: columnSize, flexShrink: 0 };
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
return { flex: 1 };
|
|
19
|
-
}
|
|
20
|
+
return { flex: 1, minWidth: 0 };
|
|
20
21
|
};
|
|
@@ -43,7 +43,7 @@ const DsTag = ({
|
|
|
43
43
|
const handleKeyDownTag = (event: KeyboardEvent<HTMLElement>) => {
|
|
44
44
|
if (onClick && (event.key === 'Enter' || event.key === ' ')) {
|
|
45
45
|
event.preventDefault();
|
|
46
|
-
onClick(event
|
|
46
|
+
onClick(event);
|
|
47
47
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
@@ -66,7 +66,7 @@ const DsTag = ({
|
|
|
66
66
|
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
67
67
|
if (onClick) {
|
|
68
68
|
event.preventDefault();
|
|
69
|
-
onClick(event
|
|
69
|
+
onClick(event);
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DsWorkspaceLayoutContextValue {
|
|
4
|
+
hasLeftSidePanel: boolean;
|
|
5
|
+
registerLeftSidePanel: () => void;
|
|
6
|
+
unregisterLeftSidePanel: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DsWorkspaceLayoutContext = createContext<DsWorkspaceLayoutContextValue | null>(null);
|
|
10
|
+
|
|
11
|
+
export const useDsWorkspaceLayoutContext = () => {
|
|
12
|
+
const context = useContext(DsWorkspaceLayoutContext);
|
|
13
|
+
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('DsWorkspaceLayout compound components must be used within DsWorkspaceLayout');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return context;
|
|
19
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
--ds-workspace-layout-header-height: 54px;
|
|
3
|
+
--ds-workspace-layout-header-gradient: radial-gradient(
|
|
4
|
+
ellipse at left bottom,
|
|
5
|
+
rgba(1, 75, 253, 1) 0%,
|
|
6
|
+
rgba(10, 60, 194, 1) 78%,
|
|
7
|
+
rgba(16, 51, 160, 1) 89%,
|
|
8
|
+
rgba(21, 42, 126, 1) 100%
|
|
9
|
+
);
|
|
10
|
+
--ds-workspace-layout-side-menu-collapsed-width: 60px;
|
|
11
|
+
--ds-workspace-layout-side-menu-expanded-width: 256px;
|
|
12
|
+
--ds-workspace-layout-side-menu-padding-top: 28px;
|
|
13
|
+
--ds-workspace-layout-side-menu-padding-x: 10px;
|
|
14
|
+
--ds-workspace-layout-content-margin-y: var(--lg);
|
|
15
|
+
--ds-workspace-layout-content-gap: var(--standard);
|
|
16
|
+
--ds-workspace-layout-content-margin-x: 40px;
|
|
17
|
+
--ds-workspace-layout-content-margin-x-with-left-panel: var(--lg);
|
|
18
|
+
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: var(--background);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.fillParent {
|
|
27
|
+
height: 100%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.header {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
height: var(--ds-workspace-layout-header-height);
|
|
35
|
+
padding: 0 var(--sm) 0 var(--lg);
|
|
36
|
+
background: var(--ds-workspace-layout-header-gradient);
|
|
37
|
+
color: var(--font-on-action);
|
|
38
|
+
overflow: clip;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.subHeader {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
padding: var(--xs) var(--standard);
|
|
46
|
+
border-bottom: 1px solid var(--border);
|
|
47
|
+
background: var(--background);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.content {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex: 1;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--ds-workspace-layout-content-gap);
|
|
55
|
+
min-width: 0;
|
|
56
|
+
min-height: 0;
|
|
57
|
+
position: relative;
|
|
58
|
+
padding: var(--ds-workspace-layout-content-margin-y) var(--ds-workspace-layout-content-margin-x);
|
|
59
|
+
overflow: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.contentWithLeftPanel {
|
|
63
|
+
padding-inline: var(--ds-workspace-layout-content-margin-x-with-left-panel);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.footer {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
padding: var(--xs) var(--standard);
|
|
71
|
+
border-top: 1px solid var(--border);
|
|
72
|
+
background: var(--background);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.body {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex: 1;
|
|
78
|
+
min-height: 0;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sideMenu {
|
|
83
|
+
position: relative;
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
height: 100%;
|
|
87
|
+
width: var(--ds-workspace-layout-side-menu-collapsed-width);
|
|
88
|
+
z-index: 100;
|
|
89
|
+
|
|
90
|
+
&[data-pinned] {
|
|
91
|
+
width: var(--ds-workspace-layout-side-menu-expanded-width);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sideMenuPanel {
|
|
96
|
+
position: relative;
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
gap: var(--xs);
|
|
100
|
+
height: 100%;
|
|
101
|
+
width: var(--ds-workspace-layout-side-menu-collapsed-width);
|
|
102
|
+
padding: var(--ds-workspace-layout-side-menu-padding-top) var(--ds-workspace-layout-side-menu-padding-x) 0;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
border-inline-end: 1px solid var(--border);
|
|
105
|
+
background: var(--background);
|
|
106
|
+
transition: width 0.2s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sideMenu[data-expanded] .sideMenuPanel {
|
|
110
|
+
width: var(--ds-workspace-layout-side-menu-expanded-width);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sideMenu[data-expanded]:not([data-pinned]) .sideMenuPanel {
|
|
114
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.sideMenuPinButton {
|
|
118
|
+
position: absolute;
|
|
119
|
+
top: var(--xs);
|
|
120
|
+
inset-inline-end: var(--xs);
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
width: 24px;
|
|
125
|
+
height: 24px;
|
|
126
|
+
padding: 0;
|
|
127
|
+
border: none;
|
|
128
|
+
border-radius: var(--3xs);
|
|
129
|
+
background: transparent;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
color: var(--font-secondary);
|
|
132
|
+
opacity: 0;
|
|
133
|
+
visibility: hidden;
|
|
134
|
+
transition:
|
|
135
|
+
opacity 0.2s,
|
|
136
|
+
visibility 0.2s,
|
|
137
|
+
background-color 0.2s,
|
|
138
|
+
color 0.2s;
|
|
139
|
+
|
|
140
|
+
&:hover {
|
|
141
|
+
background: var(--background-action-tertiary-hover);
|
|
142
|
+
color: var(--font-action);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&[aria-pressed='true'] {
|
|
146
|
+
color: var(--font-action);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
&:focus-visible {
|
|
150
|
+
outline: 2px solid var(--background-action);
|
|
151
|
+
outline-offset: 2px;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.sideMenu[data-expanded] .sideMenuPinButton {
|
|
156
|
+
opacity: 1;
|
|
157
|
+
visibility: visible;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.leftPanel {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-shrink: 0;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
min-height: 0;
|
|
165
|
+
height: 100%;
|
|
166
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import styles from './ds-workspace-layout.module.scss';
|
|
4
|
+
import { DsWorkspaceLayoutContext, useDsWorkspaceLayoutContext } from './ds-workspace-layout.context';
|
|
5
|
+
import { DsIcon } from '../ds-icon';
|
|
6
|
+
import type {
|
|
7
|
+
DsWorkspaceLayoutProps,
|
|
8
|
+
DsWorkspaceLayoutHeaderProps,
|
|
9
|
+
DsWorkspaceLayoutSubHeaderProps,
|
|
10
|
+
DsWorkspaceLayoutContentProps,
|
|
11
|
+
DsWorkspaceLayoutFooterProps,
|
|
12
|
+
DsWorkspaceLayoutBodyProps,
|
|
13
|
+
DsWorkspaceLayoutSideMenuProps,
|
|
14
|
+
DsWorkspaceLayoutLeftPanelProps,
|
|
15
|
+
} from './ds-workspace-layout.types';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_LOCALE = Object.freeze({
|
|
18
|
+
pinButtonLabel: 'Pin side menu',
|
|
19
|
+
unpinButtonLabel: 'Unpin side menu',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const DsWorkspaceLayout = ({ className, fillParent = false, ...rest }: DsWorkspaceLayoutProps) => {
|
|
23
|
+
const [hasLeftSidePanel, setHasLeftSidePanel] = useState(false);
|
|
24
|
+
|
|
25
|
+
const registerLeftSidePanel = useCallback(() => setHasLeftSidePanel(true), []);
|
|
26
|
+
const unregisterLeftSidePanel = useCallback(() => setHasLeftSidePanel(false), []);
|
|
27
|
+
|
|
28
|
+
const contextValue = useMemo(
|
|
29
|
+
() => ({ hasLeftSidePanel, registerLeftSidePanel, unregisterLeftSidePanel }),
|
|
30
|
+
[hasLeftSidePanel, registerLeftSidePanel, unregisterLeftSidePanel],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<DsWorkspaceLayoutContext.Provider value={contextValue}>
|
|
35
|
+
<div {...rest} className={classNames(styles.root, { [styles.fillParent]: fillParent }, className)} />
|
|
36
|
+
</DsWorkspaceLayoutContext.Provider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const Header = ({ className, ...rest }: DsWorkspaceLayoutHeaderProps) => (
|
|
41
|
+
<header {...rest} className={classNames(styles.header, className)} />
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const SubHeader = ({ className, ...rest }: DsWorkspaceLayoutSubHeaderProps) => (
|
|
45
|
+
<div {...rest} className={classNames(styles.subHeader, className)} />
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Content area: column layout, 24px vertical margins, 16px gap, 40px horizontal margins (24px when LeftPanel is mounted). */
|
|
49
|
+
const Content = ({ className, ...rest }: DsWorkspaceLayoutContentProps) => {
|
|
50
|
+
const { hasLeftSidePanel } = useDsWorkspaceLayoutContext();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
{...rest}
|
|
55
|
+
className={classNames(styles.content, { [styles.contentWithLeftPanel]: hasLeftSidePanel }, className)}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Footer = ({ className, ...rest }: DsWorkspaceLayoutFooterProps) => (
|
|
61
|
+
<footer {...rest} className={classNames(styles.footer, className)} />
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const Body = ({ className, ...rest }: DsWorkspaceLayoutBodyProps) => {
|
|
65
|
+
useDsWorkspaceLayoutContext();
|
|
66
|
+
|
|
67
|
+
return <div {...rest} className={classNames(styles.body, className)} />;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const SideMenu = ({
|
|
71
|
+
className,
|
|
72
|
+
pinned = false,
|
|
73
|
+
onPinnedChange,
|
|
74
|
+
locale,
|
|
75
|
+
children,
|
|
76
|
+
...rest
|
|
77
|
+
}: DsWorkspaceLayoutSideMenuProps) => {
|
|
78
|
+
useDsWorkspaceLayoutContext();
|
|
79
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
80
|
+
|
|
81
|
+
const isExpanded = pinned || isHovering;
|
|
82
|
+
const ariaLabel = pinned
|
|
83
|
+
? (locale?.unpinButtonLabel ?? DEFAULT_LOCALE.unpinButtonLabel)
|
|
84
|
+
: (locale?.pinButtonLabel ?? DEFAULT_LOCALE.pinButtonLabel);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<aside
|
|
88
|
+
{...rest}
|
|
89
|
+
className={classNames(styles.sideMenu, className)}
|
|
90
|
+
{...(pinned ? { 'data-pinned': '' } : {})}
|
|
91
|
+
{...(isExpanded ? { 'data-expanded': '' } : {})}
|
|
92
|
+
>
|
|
93
|
+
<div
|
|
94
|
+
className={styles.sideMenuPanel}
|
|
95
|
+
onMouseEnter={() => setIsHovering(true)}
|
|
96
|
+
onMouseLeave={() => setIsHovering(false)}
|
|
97
|
+
>
|
|
98
|
+
{onPinnedChange ? (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className={styles.sideMenuPinButton}
|
|
102
|
+
onClick={() => onPinnedChange(!pinned)}
|
|
103
|
+
aria-label={ariaLabel}
|
|
104
|
+
aria-pressed={pinned}
|
|
105
|
+
>
|
|
106
|
+
<DsIcon icon="keep" size="small" filled={pinned} />
|
|
107
|
+
</button>
|
|
108
|
+
) : null}
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
</aside>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const LeftPanel = ({ className, ...rest }: DsWorkspaceLayoutLeftPanelProps) => {
|
|
116
|
+
const { registerLeftSidePanel, unregisterLeftSidePanel } = useDsWorkspaceLayoutContext();
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
registerLeftSidePanel();
|
|
120
|
+
|
|
121
|
+
return unregisterLeftSidePanel;
|
|
122
|
+
}, [registerLeftSidePanel, unregisterLeftSidePanel]);
|
|
123
|
+
|
|
124
|
+
return <aside {...rest} className={classNames(styles.leftPanel, className)} />;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
DsWorkspaceLayout.Header = Header;
|
|
128
|
+
DsWorkspaceLayout.SubHeader = SubHeader;
|
|
129
|
+
DsWorkspaceLayout.Content = Content;
|
|
130
|
+
DsWorkspaceLayout.Footer = Footer;
|
|
131
|
+
DsWorkspaceLayout.Body = Body;
|
|
132
|
+
DsWorkspaceLayout.SideMenu = SideMenu;
|
|
133
|
+
DsWorkspaceLayout.LeftPanel = LeftPanel;
|
|
134
|
+
|
|
135
|
+
export default DsWorkspaceLayout;
|