diff --git a/web/packages/design/src/Box/Box.jsx b/web/packages/design/src/Box/Box.jsx
index 2645d3b568236..41f065dbf3468 100644
--- a/web/packages/design/src/Box/Box.jsx
+++ b/web/packages/design/src/Box/Box.jsx
@@ -25,6 +25,7 @@ import {
borderColor,
flex,
height,
+ lineHeight,
maxWidth,
minHeight,
maxHeight,
@@ -43,6 +44,7 @@ const Box = styled.div`
${minWidth}
${space}
${height}
+ ${lineHeight}
${minHeight}
${maxHeight}
${width}
diff --git a/web/packages/design/src/Button/Button.jsx b/web/packages/design/src/Button/Button.jsx
index 2d62d3e37c67f..39429cabff16a 100644
--- a/web/packages/design/src/Button/Button.jsx
+++ b/web/packages/design/src/Button/Button.jsx
@@ -131,6 +131,21 @@ export const kinds = props => {
background: theme.colors.buttons.warning.active,
},
};
+ case 'warning-border':
+ return {
+ color: theme.colors.buttons.warning.default,
+ background: theme.colors.buttons.border.default,
+ border: '1px solid ' + theme.colors.buttons.warning.default,
+ '&:hover, &:focus': {
+ background: theme.colors.buttons.warning.hover,
+ color: theme.colors.buttons.warning.text,
+ },
+ '&:active': {
+ background: theme.colors.buttons.warning.active,
+ color: theme.colors.buttons.warning.text,
+ },
+ };
+
case 'text':
return {
color: theme.colors.buttons.text,
@@ -238,4 +253,7 @@ export const ButtonPrimary = props => ;
export const ButtonSecondary = props => ;
export const ButtonBorder = props => ;
export const ButtonWarning = props => ;
+export const ButtonWarningBorder = props => (
+
+);
export const ButtonText = props => ;
diff --git a/web/packages/design/src/MultiRowBox/MultiRowBox.story.test.tsx b/web/packages/design/src/MultiRowBox/MultiRowBox.story.test.tsx
new file mode 100644
index 0000000000000..318d4312e9601
--- /dev/null
+++ b/web/packages/design/src/MultiRowBox/MultiRowBox.story.test.tsx
@@ -0,0 +1,33 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+import { render } from 'design/utils/testing';
+
+import { WithMultipleRows, WithSingleRow } from './MultiRowBox.story';
+
+test('renders single row', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+});
+
+test('renders multiple rows', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+});
diff --git a/web/packages/design/src/MultiRowBox/MultiRowBox.story.tsx b/web/packages/design/src/MultiRowBox/MultiRowBox.story.tsx
new file mode 100644
index 0000000000000..e4e13edb33a29
--- /dev/null
+++ b/web/packages/design/src/MultiRowBox/MultiRowBox.story.tsx
@@ -0,0 +1,41 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+import { MultiRowBox, Row, SingleRowBox } from './MultiRowBox';
+
+export default {
+ title: 'Design/MultiRowBox',
+ component: MultiRowBox,
+};
+
+export const WithMultipleRows = () => (
+
+ Part 1
+ Part 2
+ Part 3
+
+);
+
+export const WithSingleRow = () => (
+
+
Hello,
+
World!
+
+);
diff --git a/web/packages/design/src/MultiRowBox/MultiRowBox.tsx b/web/packages/design/src/MultiRowBox/MultiRowBox.tsx
new file mode 100644
index 0000000000000..2ab551cdfde7b
--- /dev/null
+++ b/web/packages/design/src/MultiRowBox/MultiRowBox.tsx
@@ -0,0 +1,65 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React, { ReactNode } from 'react';
+
+import styled from 'styled-components';
+
+import { Box } from 'design';
+
+type MultiRowBoxProps = {
+ children: ReactNode;
+};
+
+/**
+ * A box that displays a number of rows inside a rounded border, with horizontal
+ * lines between rows. Use together with {@link Row}. Example:
+ *
+ * ```tsx
+ *
+ * Row 1
+ * Row 2
+ *
+ * ```
+ */
+export const MultiRowBox = styled(Box)`
+ border: ${props =>
+ `${props.theme.borders[1]} ${props.theme.colors.interactive.tonal.neutral[2]}`};
+ border-radius: ${props => props.theme.radii[2]}px;
+`;
+
+/** A single row of a {@link MultiRowBox}. */
+export const Row = styled(Box)`
+ padding: ${props => props.theme.space[4]}px;
+ &:not(:last-child) {
+ border-bottom: ${props =>
+ `${props.theme.borders[1]} ${props.theme.colors.interactive.tonal.neutral[2]}`};
+ }
+`;
+
+/**
+ * A convenience utility to quickly render some components inside a single row
+ * with a rounded border.
+ */
+export function SingleRowBox({ children }: MultiRowBoxProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/web/packages/design/src/MultiRowBox/__snapshots__/MultiRowBox.story.test.tsx.snap b/web/packages/design/src/MultiRowBox/__snapshots__/MultiRowBox.story.test.tsx.snap
new file mode 100644
index 0000000000000..ccf8e324b5f60
--- /dev/null
+++ b/web/packages/design/src/MultiRowBox/__snapshots__/MultiRowBox.story.test.tsx.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders multiple rows 1`] = `
+.c0 {
+ box-sizing: border-box;
+}
+
+.c1 {
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+}
+
+.c2 {
+ padding: 24px;
+}
+
+.c2:not(:last-child) {
+ border-bottom: 1px solid rgba(255,255,255,0.18);
+}
+
+
+`;
diff --git a/web/packages/design/src/MultiRowBox/index.ts b/web/packages/design/src/MultiRowBox/index.ts
new file mode 100644
index 0000000000000..b9cf90aa9d738
--- /dev/null
+++ b/web/packages/design/src/MultiRowBox/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export { MultiRowBox, SingleRowBox, Row } from './MultiRowBox';
diff --git a/web/packages/design/src/system/index.js b/web/packages/design/src/system/index.js
index c08a4be06b057..3c502cfb2cb36 100644
--- a/web/packages/design/src/system/index.js
+++ b/web/packages/design/src/system/index.js
@@ -31,6 +31,7 @@ import {
height,
justifyContent,
justifySelf,
+ lineHeight,
maxHeight,
maxWidth,
minHeight,
@@ -74,6 +75,7 @@ export {
height,
justifyContent,
justifySelf,
+ lineHeight,
maxHeight,
maxWidth,
minHeight,
diff --git a/web/packages/design/src/theme/themes/bblpTheme.ts b/web/packages/design/src/theme/themes/bblpTheme.ts
index 161f97c362609..7eb116aebabb3 100644
--- a/web/packages/design/src/theme/themes/bblpTheme.ts
+++ b/web/packages/design/src/theme/themes/bblpTheme.ts
@@ -75,16 +75,18 @@ const levels = {
popout: '#373737',
};
+const neutralColors = [
+ 'rgba(255,255,255,0.07)',
+ 'rgba(255,255,255,0.13)',
+ 'rgba(255,255,255,0.18)',
+];
+
const colors: ThemeColors = {
...sharedColors,
levels,
- spotBackground: [
- 'rgba(255,255,255,0.07)',
- 'rgba(255,255,255,0.13)',
- 'rgba(255,255,255,0.18)',
- ],
+ spotBackground: neutralColors,
brand: '#FFA028',
@@ -95,6 +97,7 @@ const colors: ThemeColors = {
'rgba(255,160,40, 0.18)',
'rgba(255,160,40, 0.25)',
],
+ neutral: neutralColors,
},
},
diff --git a/web/packages/design/src/theme/themes/darkTheme.ts b/web/packages/design/src/theme/themes/darkTheme.ts
index 44399768c69a4..9d2f5e9361326 100644
--- a/web/packages/design/src/theme/themes/darkTheme.ts
+++ b/web/packages/design/src/theme/themes/darkTheme.ts
@@ -75,16 +75,18 @@ const levels = {
popout: '#4A5688',
};
+const neutralColors = [
+ 'rgba(255,255,255,0.07)',
+ 'rgba(255,255,255,0.13)',
+ 'rgba(255,255,255,0.18)',
+];
+
const colors: ThemeColors = {
...sharedColors,
levels,
- spotBackground: [
- 'rgba(255,255,255,0.07)',
- 'rgba(255,255,255,0.13)',
- 'rgba(255,255,255,0.18)',
- ],
+ spotBackground: neutralColors,
brand: '#9F85FF',
@@ -95,6 +97,7 @@ const colors: ThemeColors = {
'rgba(159,133,255, 0.18)',
'rgba(159,133,255, 0.25)',
],
+ neutral: neutralColors,
},
},
diff --git a/web/packages/design/src/theme/themes/lightTheme.ts b/web/packages/design/src/theme/themes/lightTheme.ts
index 329beb3f95df2..56dfc9addd4ad 100644
--- a/web/packages/design/src/theme/themes/lightTheme.ts
+++ b/web/packages/design/src/theme/themes/lightTheme.ts
@@ -74,12 +74,18 @@ const levels = {
popout: '#FFFFFF',
};
+const neutralColors = [
+ 'rgba(0,0,0,0.06)',
+ 'rgba(0,0,0,0.13)',
+ 'rgba(0,0,0,0.18)',
+];
+
const colors: ThemeColors = {
...sharedColors,
levels,
- spotBackground: ['rgba(0,0,0,0.06)', 'rgba(0,0,0,0.13)', 'rgba(0,0,0,0.18)'],
+ spotBackground: neutralColors,
brand: '#512FC9',
@@ -90,6 +96,7 @@ const colors: ThemeColors = {
'rgba(81,47,201, 0.18)',
'rgba(81,47,201, 0.25)',
],
+ neutral: neutralColors,
},
},
diff --git a/web/packages/design/src/theme/themes/types.ts b/web/packages/design/src/theme/themes/types.ts
index 694b12dfa17d7..fad696d7c511c 100644
--- a/web/packages/design/src/theme/themes/types.ts
+++ b/web/packages/design/src/theme/themes/types.ts
@@ -42,8 +42,9 @@ export type ThemeColors = {
/**
Spot backgrounds are used as highlights, for example
to indicate a hover or active state for an item in a menu.
+ @deprecated Use `interactive.tonal.neutral` instead.
*/
- spotBackground: [string, string, string];
+ spotBackground: string[];
brand: string;
@@ -57,6 +58,7 @@ export type ThemeColors = {
interactive: {
tonal: {
primary: string[];
+ neutral: string[];
};
};
diff --git a/web/packages/teleport/src/Account/Header.tsx b/web/packages/teleport/src/Account/Header.tsx
new file mode 100644
index 0000000000000..3539f5abe3bd3
--- /dev/null
+++ b/web/packages/teleport/src/Account/Header.tsx
@@ -0,0 +1,59 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Box, ButtonSecondary, Flex, Text } from 'design';
+import React from 'react';
+import styled, { useTheme } from 'styled-components';
+
+export interface HeaderProps {
+ title: string;
+ description?: string;
+ icon: React.ReactNode;
+ actions: React.ReactNode;
+}
+
+export function Header({ title, description, icon, actions }: HeaderProps) {
+ const theme = useTheme();
+ return (
+
+ {/* lineHeight=0 prevents the icon background from being larger than
+ required by the icon itself. */}
+
+ {icon}
+
+
+ {title}
+
+ {description}
+
+
+ {actions}
+
+ );
+}
+
+export const ActionButton = styled(ButtonSecondary)`
+ padding: ${props => `${props.theme.space[2]}px ${props.theme.space[4]}px`};
+ gap: ${props => `${props.theme.space[2]}px`};
+ text-transform: none;
+`;
diff --git a/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.story.tsx b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.story.tsx
new file mode 100644
index 0000000000000..2e8fee4a29a34
--- /dev/null
+++ b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.story.tsx
@@ -0,0 +1,102 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+import * as Icon from 'design/Icon';
+
+import { ActionButton, Header } from 'teleport/Account/Header';
+
+import { AuthDeviceList } from './AuthDeviceList';
+
+export default {
+ title: 'Teleport/Account/Manage Devices/Device List',
+};
+
+export function EmptyList() {
+ return (
+ }
+ actions={
+
+
+ Add a new device
+
+ }
+ />
+ }
+ devices={[]}
+ />
+ );
+}
+
+export function ListWithDevices() {
+ return (
+ }
+ actions={
+
+
+ Add a new device
+
+ }
+ />
+ }
+ devices={devices}
+ />
+ );
+}
+
+const devices = [
+ {
+ id: '1',
+ description: 'Hardware Key',
+ name: 'touch_id',
+ registeredDate: new Date(1628799417000),
+ lastUsedDate: new Date(1628799417000),
+ },
+ {
+ id: '2',
+ description: 'Hardware Key',
+ name: 'solokey',
+ registeredDate: new Date(1623722252000),
+ lastUsedDate: new Date(1623981452000),
+ },
+ {
+ id: '3',
+ description: 'Hardware Key',
+ name: 'backup yubikey',
+ registeredDate: new Date(1618711052000),
+ lastUsedDate: new Date(1626472652000),
+ },
+ {
+ id: '4',
+ description: 'Hardware Key',
+ name: 'yubikey',
+ registeredDate: new Date(1612493852000),
+ lastUsedDate: new Date(1614481052000),
+ },
+];
diff --git a/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.test.tsx b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.test.tsx
new file mode 100644
index 0000000000000..f39cd7318f807
--- /dev/null
+++ b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.test.tsx
@@ -0,0 +1,74 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { render, screen } from 'design/utils/testing';
+import { within } from '@testing-library/react';
+import React from 'react';
+
+import { MfaDevice } from 'teleport/services/mfa';
+
+import { AuthDeviceList } from './AuthDeviceList';
+
+const devices: MfaDevice[] = [
+ {
+ id: '1',
+ description: 'Hardware Key',
+ name: 'touch_id',
+ registeredDate: new Date(1628799417000),
+ lastUsedDate: new Date(1628799417000),
+ },
+ {
+ id: '2',
+ description: 'Hardware Key',
+ name: 'yubikey',
+ registeredDate: new Date(1623722252000),
+ lastUsedDate: new Date(1623981452000),
+ },
+];
+
+function getTableCellContents() {
+ const [header, ...rows] = screen.getAllByRole('row');
+ return {
+ header: within(header)
+ .getAllByRole('columnheader')
+ .map(cell => cell.textContent),
+ rows: rows.map(row =>
+ within(row)
+ .getAllByRole('cell')
+ .map(cell => cell.textContent)
+ ),
+ };
+}
+
+test('renders devices', () => {
+ render();
+ expect(screen.getByText('Header')).toBeInTheDocument();
+ expect(getTableCellContents()).toEqual({
+ header: ['Passkey Type', 'Nickname', 'Added', 'Last Used', 'Actions'],
+ rows: [
+ ['Hardware Key', 'touch_id', '2021-08-12', '2021-08-12', ''],
+ ['Hardware Key', 'yubikey', '2021-06-15', '2021-06-18', ''],
+ ],
+ });
+});
+
+test('renders no devices', () => {
+ render();
+ expect(screen.getByText('Header')).toBeInTheDocument();
+ expect(screen.queryAllByRole('row')).toEqual([]);
+});
diff --git a/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.tsx b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.tsx
new file mode 100644
index 0000000000000..7d754d0a804dd
--- /dev/null
+++ b/web/packages/teleport/src/Account/ManageDevices/AuthDeviceList/AuthDeviceList.tsx
@@ -0,0 +1,132 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Cell, DateCell } from 'design/DataTable';
+import Table from 'design/DataTable/Table';
+import React from 'react';
+
+import styled from 'styled-components';
+import { MultiRowBox, Row } from 'design/MultiRowBox';
+import * as Icon from 'design/Icon';
+import { ButtonWarningBorder } from 'design/Button/Button';
+
+import { MfaDevice } from 'teleport/services/mfa';
+
+export interface AuthDeviceListProps {
+ header: React.ReactNode;
+ devices: MfaDevice[];
+ onRemove?: (device: MfaDevice) => void;
+}
+
+/**
+ * Renders a table with authentication devices, preceded by a header, all inside
+ * a border.
+ */
+export function AuthDeviceList({
+ devices,
+ header,
+ onRemove,
+}: AuthDeviceListProps) {
+ return (
+
+ {header}
+ {devices.length > 0 && (
+
+
+ columns={[
+ {
+ key: 'description',
+ headerText: 'Passkey Type',
+ isSortable: true,
+ },
+ { key: 'name', headerText: 'Nickname', isSortable: true },
+ {
+ key: 'registeredDate',
+ headerText: 'Added',
+ isSortable: true,
+ render: device => ,
+ },
+ {
+ key: 'lastUsedDate',
+ headerText: 'Last Used',
+ isSortable: true,
+ render: device => ,
+ },
+ {
+ altKey: 'remove-btn',
+ headerText: 'Actions',
+ render: device => (
+ onRemove(device)} />
+ ),
+ },
+ ]}
+ data={devices}
+ emptyText=""
+ isSearchable={false}
+ initialSort={{
+ key: 'registeredDate',
+ dir: 'DESC',
+ }}
+ />
+
+ )}
+
+ );
+}
+
+interface RemoveCellProps {
+ onRemove?: () => void;
+}
+
+function RemoveCell({ onRemove }: RemoveCellProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+const StyledTable = styled(Table)(
+ props => `
+ background-color: transparent;
+
+ & > tbody > tr > td, thead > tr > th {
+ font-size: ${props.theme.fontSizes[2]}px;
+ font-weight: 300;
+ }
+
+ & > thead > tr > th {
+ text-transform: none;
+ padding-top: ${props.theme.space[2]}px;
+ padding-bottom: ${props.theme.space[2]}px;
+
+ &:first-child {
+ border-radius: ${props.theme.radii[2]}px 0 0 ${props.theme.radii[2]}px;
+ }
+ &:last-child {
+ border-radius: 0 ${props.theme.radii[2]}px ${props.theme.radii[2]}px 0;
+ }
+ }
+
+ & > tbody > tr {
+ border: none;
+ }
+`
+);