Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion core/storybook/TokensDecorator.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,56 @@
background: white;
text-align: left;
overflow: auto;
transition: opacity 0.35s ease-in-out;
transition-delay: 0.35s;
opacity: 0;
pointer-events: none;
}

._tokens-table--visible {
opacity: 1;
pointer-events: all;
transition-delay: 0.15s;
}

._tokens-table ._active {
font-weight: bold;
}

._tokens-table table {
width: 100%;
padding-bottom: 70px;
max-width: 825px;
margin: 0 auto;
}

._tokens-table td,
._tokens-table th {
box-sizing: border-box;
padding: 0.3rem 0.7rem;
padding: 0.6rem 0.7rem;
}

._tokens-table td {
border-top: 1px solid #e2e2e2;
/* letter-spacing: 0.05em; */
font-family: monospace;
}

._tokens-table td[colspan="2"] {
padding: 1.8rem 0.7rem 0.6rem;
}

._token-table__token-cell {
display: flex;
justify-content: space-between;
align-items: center;
}

._token-table__token-cell button {
background-color: #f2f2f2;
border: none;
color: #666;
font-size: 12px;
padding: 4px 6px;
cursor: pointer;
}
77 changes: 60 additions & 17 deletions core/storybook/TokensDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,78 @@ import './TokensDecorator.css';
import '../../dist/design-system/index.css';

const TokensDecorator = (props) => {
const [isTokensVisible, setIsTokensVisible] = React.useState(false);
const [lastEdit, setLastEdit] = React.useState('')

const tokenRegex = /var\(--dcx-[-_#:,\w\s]*\)/gm;
const matches = String(props.style).match(tokenRegex);
const pairs = matches.map(match => {
const matches = String(props.style).match(tokenRegex).map(match => match.replace(', ', ','));

const uniqueMatches = new Set(matches);

const tokenTuples = Array.from(uniqueMatches.values()).sort().map(match => {
const parsedMatch = match.replace(/^var\(/, '').replace(/\)$/, '');
const parts = parsedMatch.split(',');
const [key, ...defaultValue] = parts;
return [key, defaultValue.join(',')];
});

const [isTokensVisible, setIsTokensVisible] = React.useState(false);
const tokensByScope = tokenTuples.reduce((acc, tuple) => {
const [scope] = tuple[0].replace('--dcx-', '').split('-');
if (!acc[scope]) {
acc[scope] = []
}
acc[scope].push(tuple)
return acc;
}, {})

const handleTokenSelection = (scope, token) => {
const $scopeButton = parent.document.querySelector(`#tabbutton-${scope}`);
if (!$scopeButton) {
return
}

$scopeButton.click();

setTimeout(() => {
const $tokenSearch = parent.document.querySelector(`.token-search input`);

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value').set;

nativeInputValueSetter.call($tokenSearch, token);
$tokenSearch.dispatchEvent(new Event('change', { bubbles: true }));
setIsTokensVisible(false);
setLastEdit(token);
}, 100)
}

return (
<div>
{isTokensVisible && (
<div className='_tokens-table'>
<table className='foo'>
<tr>
<th>Token</th>
<th>Default Value</th>
</tr>
{pairs.map(pair => (
<div className={`_tokens-table ${isTokensVisible && '_tokens-table--visible'}`}>
<table>
{Object.entries(tokensByScope).map((entry) => (
<React.Fragment>
<tr>
<td>{pair[0]}</td>
<td>{pair[1]}</td>
<td colSpan={2}>{entry[0]}</td>
</tr>
))}
</table>
</div>
)}
{entry[1].map((pair) => (
<tr>
<td>
<div className='_token-table__token-cell'>
<span className={lastEdit === pair[0] ? '_active' : ''}>{pair[0]}</span>
<button onClick={handleTokenSelection.bind(this, entry[0], pair[0])}>
Edit
</button>
</div>
</td>
<td>{pair[1]}</td>
</tr>
))}
</React.Fragment>
))}
</table>
</div>
<button
className='_tokens-button'
onClick={() => setIsTokensVisible(!isTokensVisible)}
Expand Down
41 changes: 41 additions & 0 deletions core/storybook/TokensList/TokensList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Details } from '../../../src/details'
import tokens from '../../../src/design-system/tokens.json'

const TokensList = () => {
const tokensByScope = Object.entries(tokens).reduce((acc, entry) => {
const tokenName = entry[0]
const [tokenScope] = tokenName.split('-')
if (!acc[tokenScope]) {
acc[tokenScope] = []
}
acc[tokenScope].push(entry)
return acc
}, {})

return (
Object.entries(tokensByScope).map(entry => (
<Details summary={entry[0]}>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
<table style={{ margin: '0px', width: '100%' }}>
<thead>
<tr>
<td>Token</td>
<td>Default value</td>
</tr>
</thead>
<tbody>
{entry[1].map(tokenEntry => (
<tr key={tokenEntry[0]}>
<td>--dcx-{tokenEntry[0]}</td>
<td>{tokenEntry[1]}</td>
</tr>
))}
</tbody>
</table>
</div>
</Details>
))
)
}

export default TokensList
1 change: 1 addition & 0 deletions core/storybook/TokensList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './TokensList'
3 changes: 2 additions & 1 deletion src/tabGroup/components/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const Tab = ({
event:
| React.MouseEvent<HTMLAnchorElement>
| React.TouchEvent<HTMLAnchorElement>
) => changeActiveTab(event.currentTarget.dataset.tabId as string);
) => changeActiveTab(event.currentTarget.dataset.tabId as string);

return (
<li role={Roles.presentation} className={classes}>
Expand All @@ -84,6 +84,7 @@ export const Tab = ({
tabIndex={!selected ? -1 : 0}
onClick={!disabled ? onClickHandler : undefined}
href={`#${eventKey}`}
target='_self'
>
{label}
</a>
Expand Down
Binary file added static/design-system-inspect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/design-system-playground.mp4
Binary file not shown.
163 changes: 163 additions & 0 deletions stories/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Meta } from '@storybook/blocks';
import TokensList from '../core/storybook/TokensList';

<Meta title="DCXLibrary/Introduction" />

Expand Down Expand Up @@ -43,3 +44,165 @@ We don't ship `dcx-react-library` with any included CSS. However, some styleshee
/* The following line can be included in your src/index.js or App.js file */
import '@capgeminiuk/dcx-react-library/dist/dcx-react-library.css';
```

## Design System

The `dcx-react-library` supports opt-in styling customizable via [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).

To keep the library **UI/UX agnostic** the styles are not shipped by default. In order to include them in your project you can import them as follows:

```jsx
/* main.jsx */

import '@capgeminiuk/dcx-react-library/design-system/index.css'
```

Note that this will setup the styling for all the components in the library. You may want to fine-tune the imports if performance is critical for your application.

All the components have their own stylesheet and can be imported individually when needed:

```jsx
/* Needed for core styles */
import '@capgeminiuk/dcx-react-library/design-system/base.css'

/* Individual imports, works well with tree-shaking */
import '@capgeminiuk/dcx-react-library/design-system/form-control.css'
import '@capgeminiuk/dcx-react-library/design-system/button.css'
// ...
```

Don't forget to add the `base.css` import before loading the individual stylesheets. It contains the core definitions used for the design system to work.


### Customization

Loading the stylesheets will apply a neutral style resembling Material guidelines by default. There are 2 ways for further customizing the library:

* [CSS custom properties](#css-custom-properties)
* [Regular CSS styling](#regular-css-styling)

### CSS custom properties

This is the recommended way of customizing the library, you may think of it as the library's API, it provides a way of coherently styling all the related elements.

All custom properties follow the same pattern:

```
[scope]-[property]-[element]-[variant]-[state]
```

`scope`

Defines where the scope custom property will affect. Can be one of the following:

* border: box-model's border width, color, radius, etc.
* color: element's color, backgound color, etc.
* font: element's font family, size, line height, etc.
* spacing: box-model's padding

`property`

Defines the actual element's property that will be modified. Some examples are:

* text: element's font color
* background: box-model's background
* size: element's font size
* etc.

`element`

Defines the actual element that will be affected.

**There is a limited amount of elements**, there is not a 1 to 1 correlation with each avaialble component.

The reasoning behind this is that even if we have different components they usually are styled together in a consistent maner. For example the checkbox, radio button and input all fall under the `forcontrol` element, they should have consistent colors, spacing, font sizes, etc.

These are the elements that support styling:

* action: interactive components that are triggered by the user (buttons, tabs, etc.)
* formcontrol: components that capture the user's input (checkbox, radio button, etc.)
* heading: all the layout headings (h1, h2, h3, etc.)
* body: layout's main content (p, span, code, etc.)
* link: components that can trigger a navigation (anchors, etc.)

`variant` (Optional)

Applies to components that support variants of the main element. For example an `Input` component can have a `floating` variant that animates the label when it receives user input.

`state` (Optional)

Applies to components that are stateful. For example a `Button` component can have a `hover` state. You can expect to find these definitions for interactive elements.

#### Defining custom properties

In order to consistently apply the styles acros the application you may wan to use the `:root` selector in your main stylesheet.

```css
/* main.css - your root CSS file */

:root {
--dcx-color-text-action-default-primary: #212121;
}
```

The custom properties follow the same specifity rules as regular CSS. This means it is possible to overwrite the values for specific parts of the application or when conditions are met:

```css
body.dark-theme {
--dcx-color-text-action-default-primary: orange;
}

.client-sidepanel {
--dcx-color-text-action-default-primary: green;
}
```

#### Available custom properties

The following custom properties are available under the scopes:

<TokensList />

### Component's Design System

All components that support the Design System opt-in will display a `Design system` folder. Here you can find the Playground and predefined themes.

##### Inspecting the components

All components that support the design system will have the token definitions displayed under the debugger `styles` tab.

![Inspecting the design system](/design-system-inspect.png)

##### Playground

It is an interactive UI that displays the available custom properties for a given component.

To find the properties list use the `Toggle Available Tokens` button, then you can use the filter by scope and apply your own values for a quick preview.

<video src="/design-system-playground.mp4" style={{width: '100%'}} controls />


##### Predefined themes

Right next to the `Playground` you will usually find themes like: `Dark`, `Material` and `Accessible`. You may want to have a look to get inspiration for your own token definition.

### Regular CSS styling

The Design System's purpose is to define the base styling of your UI, you can further fine tune the appearance by using custom CSS.

```css
.my-floating-button {
width: 100%;
position: fixed;
bottom: 15px;
right: 15px;
}
```

Your custom styles will overwrite the ones defined by the Design System when applied on specific elements:

```jsx
<Button className="my-floating-button">My Floating Button</Button>
```

**Make sure that your custom stylesheets are imported after the ones imported from `@capgeminiuk`** to avoid CSS specifity problems.