[MIRROR] Fix ExoFab UI and Section bugs, improve Tabs, upgrade deps (#3213)

* Fix ExoFab UI and Section bugs, improve Tabs, upgrade deps (#56721)

    Fixes #56237
    Unblocks #56704

Besides fixing the above mentioned issue with ExoFab UI, I fixed a few tgui component bugs and added a few improvements. Not really atomizable, sorry.
Section changes

    level property is now gone, section level is automatically calculated.
    Nested sections automatically become transparent.
    Nested sections are now properly incapsulated and won't inherit properties of parent sections.
        This allows using nested non-scrollable sections, while parent is, for example, scrollable.

Tabs changes

    Tabs without a parent section will pretend they are a section (and will look as such, with dark background).
    Top corners of tabs are a bit rounded to make it look more like a tab.
    Background color for selected and hovered tabs (previously it was just an underline).
    2px margin around the tabs, to make them stand out a bit from the section background.

Two new props:

    fill - Same as <Section fill>
    fluid - Tabs use full width of the container.

scrollable property was not implemented because it's a bit complicated (requires converting it into a class, registering its scrollable node with tgui, etc). If you need scrollable, just wrap tabs with a Section like a good man.

* Fix ExoFab UI and Section bugs, improve Tabs, upgrade deps

Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
This commit is contained in:
SkyratBot
2021-02-08 23:49:17 +00:00
committed by GitHub
co-authored by Aleksej Komarov
parent 3fb96da99a
commit e7573ea80f
15 changed files with 979 additions and 815 deletions
+28 -10
View File
@@ -838,10 +838,12 @@ If you want to have a button on the right side of an section title
</Section>
```
**New:** Sections can now be nested, and will automatically font size of the
header according to their nesting level. Previously this was done via `level`
prop, but now it is automatically calculated.
- See inherited props: [Box](#box)
- `title: string` - Title of the section.
- `level: number` - Section level in hierarchy. Default is 1, higher number
means deeper level of nesting. Must be an integer number.
- `buttons: any` - Buttons to render aside the section title.
- `fill: boolean` - If true, fills all available vertical space.
- `fitted: boolean` - If true, removes all section padding.
@@ -1025,25 +1027,41 @@ Notice that tabs do not contain state. It is your job to track the selected
tab, handle clicks and place tab content where you need it. In return, you get
a lot of flexibility in regards to how you can layout your tabs.
Tabs also support a vertical configuration. This is usually paired with a
[Flex](#flex) component to render tab content to the right.
Tabs also support a vertical configuration. This is usually paired with
[Stack](#stack) to render tab content to the right.
```jsx
<Flex>
<Flex.Item>
<Stack>
<Stack.Item>
<Tabs vertical>
...
</Tabs>
</Flex.Item>
<Flex.Item grow={1} basis={0}>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
Tab content.
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
```
If you need to combine a tab section with other elements, or if you want to
add scrollable functionality to tabs, pair them with the [Section](#section)
component:
```jsx
<Section fill fitted scrollable width="128px">
<Tabs vertical>
...
</Tabs>
... other things ...
</Section>
```
**Props:**
- See inherited props: [Box](#box)
- `fluid: boolean` - If true, tabs will take all available horizontal space.
- `fill: boolean` - Similarly to `fill` on [Section](#section), tabs will fill
all available vertical space. Only makes sense in a vertical configuration.
- `vertical: boolean` - Use a vertical configuration, where tabs will be
stacked vertically.
- `children: Tab[]` - This component only accepts tabs as its children.
+11 -11
View File
@@ -6,28 +6,28 @@
"packages/*"
],
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.1",
"@babel/plugin-transform-jscript": "^7.12.1",
"@babel/preset-env": "^7.12.11",
"@babel/core": "^7.12.13",
"@babel/eslint-parser": "^7.12.13",
"@babel/plugin-transform-jscript": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"babel-loader": "^8.2.2",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^5.0.1",
"cssnano": "^4.1.10",
"eslint": "^7.18.0",
"eslint": "^7.19.0",
"eslint-plugin-react": "^7.22.0",
"file-loader": "^6.2.0",
"inferno": "^7.4.7",
"mini-css-extract-plugin": "^1.3.4",
"sass": "^1.32.4",
"sass-loader": "^10.1.1",
"mini-css-extract-plugin": "^1.3.5",
"sass": "^1.32.6",
"sass-loader": "^11.0.0",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"url-loader": "^4.1.1",
"webpack": "^5.15.0",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-cli": "^4.3.1"
"webpack": "^5.21.1",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
}
}
+1 -1
View File
@@ -9,6 +9,6 @@
"glob": "^7.1.6",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.10",
"ws": "^7.4.2"
"ws": "^7.4.3"
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ export const Panel = (props, context) => {
<Stack fill vertical>
<Stack.Item>
<Section fitted>
<Stack mx={1} align="center">
<Stack mr={1} align="center">
<Stack.Item grow overflowX="auto">
<ChatTabs />
</Stack.Item>
@@ -36,13 +36,7 @@
'text-color': rgba(0, 0, 0, 0.5),
'color-default': rgba(0, 0, 0, 1),
));
@include meta.load-css('~tgui/styles/components/Section.scss', $with: (
// 'background-color': rgba(0, 0, 0, 0.1),
'shadow-color': rgba(0, 0, 0, 0.1),
'shadow-size': 1em,
'shadow-type': outset,
'shadow-offset': 0 0.2em,
));
@include meta.load-css('~tgui/styles/components/Section.scss');
@include meta.load-css('~tgui/styles/components/Button.scss', $with: (
'color-default': #bbbbbb,
'color-disabled': #363636,
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "tgui-polyfill",
"version": "4.3.0",
"dependencies": {
"core-js": "^3.8.2",
"core-js": "^3.8.3",
"regenerator-runtime": "^0.13.7",
"whatwg-fetch": "^3.5.0"
}
+6 -16
View File
@@ -12,19 +12,19 @@ import { computeBoxClassName, computeBoxProps } from './Box';
export class Section extends Component {
constructor(props) {
super(props);
this.ref = createRef();
this.scrollableRef = createRef();
this.scrollable = props.scrollable;
}
componentDidMount() {
if (this.scrollable) {
addScrollableNode(this.ref.current);
addScrollableNode(this.scrollableRef.current);
}
}
componentWillUnmount() {
if (this.scrollable) {
removeScrollableNode(this.ref.current);
removeScrollableNode(this.scrollableRef.current);
}
}
@@ -32,7 +32,6 @@ export class Section extends Component {
const {
className,
title,
level = 1,
buttons,
fill,
fitted,
@@ -41,21 +40,10 @@ export class Section extends Component {
...rest
} = this.props;
const hasTitle = canRender(title) || canRender(buttons);
const content = fitted
? children
: (
<div
ref={this.ref}
className="Section__content">
{children}
</div>
);
return (
<div
ref={fitted ? this.ref : undefined}
className={classes([
'Section',
'Section--level--' + level,
Byond.IS_LTE_IE8 && 'Section--iefix',
fill && 'Section--fill',
fitted && 'Section--fitted',
@@ -75,7 +63,9 @@ export class Section extends Component {
</div>
)}
<div className="Section__rest">
{content}
<div ref={this.scrollableRef} className="Section__content">
{children}
</div>
</div>
</div>
);
+2
View File
@@ -12,6 +12,7 @@ export const Tabs = props => {
const {
className,
vertical,
fill,
fluid,
children,
...rest
@@ -23,6 +24,7 @@ export const Tabs = props => {
vertical
? 'Tabs--vertical'
: 'Tabs--horizontal',
fill && 'Tabs--fill',
fluid && 'Tabs--fluid',
className,
computeBoxClassName(rest),
@@ -386,20 +386,22 @@ const PartLists = (props, context) => {
</Section>
</Stack.Item>
<Stack.Item grow>
{!!searchText && (
<PartCategory
name={"Search Results"}
parts={partsList}
forceShow
placeholder="No matching results..." />
) || (
Object.keys(partsList).map(category => (
<Section fill scrollable>
{!!searchText && (
<PartCategory
key={category}
name={category}
parts={partsList[category]} />
))
)}
name={"Search Results"}
parts={partsList}
forceShow
placeholder="No matching results..." />
) || (
Object.keys(partsList).map(category => (
<PartCategory
key={category}
name={category}
parts={partsList[category]} />
))
)}
</Section>
</Stack.Item>
</Stack>
);
@@ -419,77 +421,77 @@ const PartCategory = (props, context) => {
const [
displayMatCost,
] = useSharedState(context, 'display_mats', false);
if (!forceShow && parts.length === 0) {
return null;
}
return (
((!!parts.length || forceShow) && (
<Section
fill
scrollable
title={name}
buttons={
<Button
disabled={!parts.length}
color="good"
content="Queue All"
icon="plus-circle"
onClick={() => act("add_queue_set", {
part_list: parts.map(part => part.id),
})} />
}>
{!parts.length && placeholder}
{parts.map(part => (
<Fragment key={part.name}>
<Stack align="center">
<Stack.Item>
<Button
disabled={(
buildingPart || part.format.textColor === COLOR_BAD
)}
color="good"
icon="play"
onClick={() => act("build_part", { id: part.id })} />
</Stack.Item>
<Stack.Item>
<Button
color="average"
icon="plus-circle"
onClick={() => act("add_queue_part", { id: part.id })} />
</Stack.Item>
<Stack.Item grow color={COLOR_KEYS[part.format.textColor]}>
{part.name}
</Stack.Item>
<Stack.Item>
<Button
icon="question-circle"
tooltip={
'Build Time: '
+ part.printTime + 's. '
+ (part.desc || '')
}
tooltipPosition="left" />
</Stack.Item>
<Section
title={name}
level={2}
buttons={
<Button
disabled={!parts.length}
color="good"
content="Queue All"
icon="plus-circle"
onClick={() => act("add_queue_set", {
part_list: parts.map(part => part.id),
})} />
}>
{!parts.length && placeholder}
{parts.map(part => (
<Fragment key={part.name}>
<Stack align="center">
<Stack.Item>
<Button
disabled={(
buildingPart || part.format.textColor === COLOR_BAD
)}
color="good"
icon="play"
onClick={() => act("build_part", { id: part.id })} />
</Stack.Item>
<Stack.Item>
<Button
color="average"
icon="plus-circle"
onClick={() => act("add_queue_part", { id: part.id })} />
</Stack.Item>
<Stack.Item grow color={COLOR_KEYS[part.format.textColor]}>
{part.name}
</Stack.Item>
<Stack.Item>
<Button
icon="question-circle"
tooltip={
'Build Time: '
+ part.printTime + 's. '
+ (part.desc || '')
}
tooltipPosition="left" />
</Stack.Item>
</Stack>
{displayMatCost && (
<Stack mb={2}>
{Object.keys(part.cost).map(material => (
<Stack.Item
key={material}
width="50px"
color={COLOR_KEYS[part.format[material].color]}>
<MaterialAmount
formatmoney
style={{
transform: 'scale(0.75) translate(0%, 10%)',
}}
name={material}
amount={part.cost[material]} />
</Stack.Item>
))}
</Stack>
{displayMatCost && (
<Stack mb={2}>
{Object.keys(part.cost).map(material => (
<Stack.Item
key={material}
width="50px"
color={COLOR_KEYS[part.format[material].color]}>
<MaterialAmount
formatmoney
style={{
transform: 'scale(0.75) translate(0%, 10%)',
}}
name={material}
amount={part.cost[material]} />
</Stack.Item>
))}
</Stack>
)}
</Fragment>
))}
</Section>
))
)}
</Fragment>
))}
</Section>
);
};
+1 -1
View File
@@ -7,7 +7,7 @@
"dompurify": "^2.2.6",
"inferno": "^7.4.7",
"inferno-vnode-flags": "^7.4.7",
"marked": "^1.2.7",
"marked": "^1.2.9",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
+48 -33
View File
@@ -5,7 +5,7 @@
*/
import { useLocalState } from '../backend';
import { Button, Section, Tabs } from '../components';
import { Box, Button, Divider, Section, Tabs } from '../components';
export const meta = {
title: 'Tabs',
@@ -20,7 +20,6 @@ const TAB_RANGE = [
];
const Story = (props, context) => {
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [tabProps, setTabProps] = useLocalState(context, 'tabProps', {});
return (
<>
@@ -67,43 +66,59 @@ const Story = (props, context) => {
})} />
<Button.Checkbox
inline
content="left aligned"
checked={tabProps.leftAligned}
content="centered"
checked={tabProps.centered}
onClick={() => setTabProps({
...tabProps,
leftAligned: !tabProps.leftAligned,
centered: !tabProps.centered,
})} />
</Section>
<Section fitted>
<Tabs
vertical={tabProps.vertical}
fluid={tabProps.fluid}
textAlign={tabProps.leftAligned && 'left'}>
{TAB_RANGE.map((text, i) => (
<Tabs.Tab
key={i}
selected={i === tabIndex}
icon={tabProps.icon && 'info-circle'}
leftSlot={tabProps.leftSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
rightSlot={tabProps.rightSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
onClick={() => setTabIndex(i)}>
{text}
</Tabs.Tab>
))}
</Tabs>
<TabsPrefab />
</Section>
<Section title="Normal section">
<TabsPrefab />
Some text
</Section>
<Section>
Section-less tabs appear the same as tabs in a fitted section:
</Section>
<TabsPrefab />
</>
);
};
const TabsPrefab = (props, context) => {
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [tabProps] = useLocalState(context, 'tabProps', {});
return (
<Tabs
vertical={tabProps.vertical}
fluid={tabProps.fluid}
textAlign={tabProps.centered && 'center'}>
{TAB_RANGE.map((text, i) => (
<Tabs.Tab
key={i}
selected={i === tabIndex}
icon={tabProps.icon && 'info-circle'}
leftSlot={tabProps.leftSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
rightSlot={tabProps.rightSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
onClick={() => setTabIndex(i)}>
{text}
</Tabs.Tab>
))}
</Tabs>
);
};
@@ -11,10 +11,6 @@
$title-text-color: base.$color-fg !default;
$background-color: base.$color-bg-section !default;
$separator-color: colors.$primary !default;
$shadow-color: rgba(0, 0, 0, 0.5) !default;
$shadow-type: inset !default;
$shadow-size: 0.5em !default;
$shadow-offset: 0 0 !default;
.Section {
position: relative;
@@ -55,21 +51,25 @@ $shadow-offset: 0 0 !default;
padding: 0.66em 0.5em;
}
.Section--fitted > .Section__rest > .Section__content {
padding: 0;
}
.Section--fill {
display: flex;
flex-direction: column;
height: 100%;
}
.Section--fill .Section__rest {
.Section--fill > .Section__rest {
flex-grow: 1;
.Section__content {
height: 100%;
}
}
.Section--fill.Section--scrollable .Section__content {
.Section--fill > .Section__rest > .Section__content {
height: 100%;
}
.Section--fill.Section--scrollable > .Section__rest > .Section__content {
position: absolute;
top: 0;
left: 0;
@@ -77,14 +77,14 @@ $shadow-offset: 0 0 !default;
bottom: 0;
}
.Section--iefix.Section--fill {
.Section--fill.Section--iefix {
display: table !important;
width: 100% !important;
height: 100% !important;
border-collapse: collapse;
border-spacing: 0;
& .Section__rest {
& > .Section__rest {
display: table-row !important;
height: 100% !important;
}
@@ -93,29 +93,31 @@ $shadow-offset: 0 0 !default;
.Section--scrollable {
overflow-x: hidden;
overflow-y: hidden;
& > .Section__rest > .Section__content {
overflow-y: scroll;
overflow-x: hidden;
}
}
.Section--scrollable .Section__content {
overflow-y: scroll;
overflow-x: hidden;
// Nested sections
.Section .Section {
background-color: transparent;
margin-left: -0.5em;
margin-right: -0.5em;
// Remove extra space above the first nested section
&:first-child {
margin-top: -0.5em;
}
}
.Section--level--1 .Section__titleText {
font-size: base.em(14px);
}
.Section--level--2 .Section__titleText {
// Level 2 section title
.Section .Section .Section__titleText {
font-size: base.em(13px);
}
.Section--level--3 .Section__titleText {
// Level 3 section title
.Section .Section .Section .Section__titleText {
font-size: base.em(12px);
}
.Section--level--2,
.Section--level--3 {
background-color: transparent;
box-shadow: none;
margin-left: -0.5em;
margin-right: -0.5em;
}
@@ -10,6 +10,9 @@
$color-default: color.scale(colors.fg(colors.$primary), $lightness: 75%) !default;
$text-color: rgba(255, 255, 255, 0.5) !default;
$text-color-selected: color.scale($color-default, $lightness: 25%) !default;
$tab-color: transparent !default;
$tab-color-hovered: rgba(255, 255, 255, 0.075) !default;
$tab-color-selected: rgba(255, 255, 255, 0.125) !default;
$border-radius: base.$border-radius !default;
$fg-map: colors.$fg-map !default;
@@ -17,14 +20,34 @@ $fg-map: colors.$fg-map !default;
display: flex;
align-items: stretch;
overflow: hidden;
background-color: base.$color-bg-section;
}
.Tabs--fill {
height: 100%;
}
// Interoperability with sections
.Section .Tabs {
background-color: transparent;
}
.Section:not(.Section--fitted) .Tabs {
margin: 0 -0.5em 0.5em;
&:first-child {
margin-top: -0.5em;
}
}
.Tabs--vertical {
flex-direction: column;
padding: 0.25em 0 0.25em 0.25em;
}
.Tabs--horizontal {
margin-bottom: 0.5em;
padding: 0.25em 0.25em 0 0.25em;
&:last-child {
margin-bottom: 0;
@@ -43,12 +66,20 @@ $fg-map: colors.$fg-map !default;
display: flex;
align-items: center;
justify-content: space-between;
background-color: $tab-color;
color: $text-color;
min-height: 2.25em;
min-width: 4em;
transition: background-color 50ms ease-out;
}
.Tab:not(.Tab--selected):hover {
background-color: $tab-color-hovered;
transition: background-color 0;
}
.Tab--selected {
background-color: $tab-color-selected;
color: $text-color-selected;
}
@@ -73,6 +104,8 @@ $fg-map: colors.$fg-map !default;
.Tab {
border-top: (1em / 6) solid transparent;
border-bottom: (1em / 6) solid transparent;
border-top-left-radius: 0.25em;
border-top-right-radius: 0.25em;
}
.Tab--selected {
@@ -85,6 +118,8 @@ $fg-map: colors.$fg-map !default;
min-height: 2em;
border-left: (1em / 6) solid transparent;
border-right: (1em / 6) solid transparent;
border-top-left-radius: 0.25em;
border-bottom-left-radius: 0.25em;
}
.Tab--selected {
@@ -33,7 +33,6 @@ $font-size: 24px;
@include meta.load-css('../components/Tabs.scss');
@include meta.load-css('../components/Section.scss', $with: (
'background-color': rgba(0, 0, 0, 0.1),
'shadow-color': rgba(0, 0, 0, 0.2),
));
@include meta.load-css('../components/Button.scss', $with: (
'color-default': #E8E4C9,
+729 -622
View File
File diff suppressed because it is too large Load Diff