@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.cjs +1097 -308
  3. package/dist/index.d.cts +1118 -982
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1118 -982
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1089 -309
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +31 -31
  11. package/src/components/ds-date-input/ds-date-input.utils.ts +2 -2
  12. package/src/components/ds-drawer/ds-drawer.tsx +2 -0
  13. package/src/components/ds-form-control/ds-form-control.tsx +14 -11
  14. package/src/components/ds-form-control/ds-form-control.types.ts +4 -0
  15. package/src/components/ds-grid/ds-grid.tsx +1 -2
  16. package/src/components/ds-modal/ds-modal.tsx +3 -0
  17. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +21 -2
  18. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +14 -11
  19. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +1 -6
  20. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +2 -2
  21. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +2 -0
  22. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.tsx +42 -11
  23. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +24 -4
  24. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +22 -1
  25. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +22 -0
  26. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +1 -0
  27. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +19 -0
  28. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -1
  29. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +1 -1
  30. package/src/components/ds-table/components/edit/cell-editors/cell-editors.module.scss +3 -0
  31. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +27 -0
  32. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +50 -0
  33. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +51 -0
  34. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +36 -0
  35. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +50 -0
  36. package/src/components/ds-table/components/edit/cell-editors/index.ts +14 -0
  37. package/src/components/ds-table/components/edit/cell-editors/table-edit-form-control.tsx +31 -0
  38. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +28 -0
  39. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +39 -0
  40. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.types.ts +7 -0
  41. package/src/components/ds-table/components/edit/ds-table-editable-cell/index.ts +2 -0
  42. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.module.scss +48 -0
  43. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +76 -0
  44. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.types.ts +5 -0
  45. package/src/components/ds-table/components/edit/ds-table-editing-cell/index.ts +2 -0
  46. package/src/components/ds-table/context/ds-table-context.ts +79 -1
  47. package/src/components/ds-table/ds-table-column.types.ts +12 -0
  48. package/src/components/ds-table/ds-table.module.scss +100 -10
  49. package/src/components/ds-table/ds-table.tsx +18 -19
  50. package/src/components/ds-table/ds-table.types.ts +52 -0
  51. package/src/components/ds-table/filters/adapters/create-checkbox-filter-adapter.tsx +24 -40
  52. package/src/components/ds-table/filters/adapters/create-dual-range-filter-adapter.tsx +10 -31
  53. package/src/components/ds-table/filters/adapters/create-filter-adapter.ts +6 -98
  54. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.module.scss +36 -0
  55. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.tsx +92 -13
  56. package/src/components/ds-table/filters/components/checkbox-filter/index.ts +1 -1
  57. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.module.scss +42 -0
  58. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx +89 -0
  59. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.types.ts +25 -0
  60. package/src/components/ds-table/filters/components/column-filter-popover/index.ts +5 -0
  61. package/src/components/ds-table/filters/hooks/use-table-filters.ts +113 -78
  62. package/src/components/ds-table/filters/index.ts +2 -44
  63. package/src/components/ds-table/filters/types/column-meta.augmentation.ts +19 -0
  64. package/src/components/ds-table/filters/types/filter-adapter.types.ts +48 -81
  65. package/src/components/ds-table/hooks/use-cell-editor.ts +81 -0
  66. package/src/components/ds-table/index.ts +29 -0
  67. package/src/components/ds-table/utils/column-size.ts +9 -8
  68. package/src/components/ds-tag/ds-tag.tsx +2 -2
  69. package/src/components/ds-workspace-layout/ds-workspace-layout.context.ts +19 -0
  70. package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +166 -0
  71. package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +135 -0
  72. package/src/components/ds-workspace-layout/ds-workspace-layout.types.ts +52 -0
  73. package/src/components/ds-workspace-layout/index.ts +11 -0
  74. package/src/index.ts +1 -1
  75. package/src/styles/_scrollbars.scss +37 -0
  76. package/src/utils/restore-document-body-modal-styles.ts +44 -0
  77. package/src/utils/use-ark-dialog-body-lock-cleanup.ts +36 -0
  78. package/src/components/ds-workspace/ds-workspace.module.scss +0 -55
  79. package/src/components/ds-workspace/ds-workspace.tsx +0 -37
  80. package/src/components/ds-workspace/ds-workspace.types.ts +0 -17
  81. 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 { ChipItem } from '../../../ds-chip-group';
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
- * Base filter adapter interface that all filters must implement
10
- * This provides a consistent contract for filter behavior
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
- * Calculate how many active filters are applied
59
- * Used for the count indicator in filter navigation
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 and virtualization state
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
- * @param virtualized - Whether the table is virtualized
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, virtualized?: boolean): CSSProperties | undefined => {
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
- if (virtualized) {
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 as KeyboardEvent<HTMLDivElement>);
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 as MouseEvent<HTMLDivElement>);
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;