actual tgui4

This commit is contained in:
Letter N
2020-07-21 21:00:18 +08:00
parent d64c240da3
commit 26cccd84c1
66 changed files with 2192 additions and 694 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { loadCSS as fgLoadCSS } from 'fg-loadcss';
import { createLogger } from './logging';
const logger = createLogger('assets');
const EXCLUDED_PATTERNS = [
/v4shim/i,
];
const loadedStyles = [];
const loadedMappings = {};
export const loadCSS = url => {
if (loadedStyles.includes(url)) {
return;
}
loadedStyles.push(url);
logger.log(`loading stylesheet '${url}'`);
fgLoadCSS(url);
};
export const resolveAsset = name => (
loadedMappings[name] || name
);
export const assetMiddleware = store => next => action => {
const { type, payload } = action;
if (type === 'asset/stylesheet') {
loadCSS(payload);
return;
}
if (type === 'asset/mappings') {
for (let name of Object.keys(payload)) {
// Skip anything that matches excluded patterns
if (EXCLUDED_PATTERNS.some(regex => regex.test(name))) {
continue;
}
const url = payload[name];
const ext = name.split('.').pop();
loadedMappings[name] = url;
if (ext === 'css') {
loadCSS(url);
}
}
return;
}
next(action);
};
+194 -27
View File
@@ -11,8 +11,12 @@
* @license MIT
*/
import { perf } from 'common/perf';
import { UI_DISABLED, UI_INTERACTIVE } from './constants';
import { callByond } from './byond';
import { releaseHeldKeys } from './hotkeys';
import { createLogger } from './logging';
const logger = createLogger('backend');
export const backendUpdate = state => ({
type: 'backend/update',
@@ -24,7 +28,23 @@ export const backendSetSharedState = (key, nextState) => ({
payload: { key, nextState },
});
export const backendReducer = (state, action) => {
export const backendSuspendStart = () => ({
type: 'backend/suspendStart',
});
export const backendSuspendSuccess = () => ({
type: 'backend/suspendSuccess',
payload: {
timestamp: Date.now(),
},
});
const initialState = {
config: {},
data: {},
};
export const backendReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'backend/update') {
@@ -63,6 +83,7 @@ export const backendReducer = (state, action) => {
shared,
visible,
interactive,
suspended: false,
};
}
@@ -77,30 +98,175 @@ export const backendReducer = (state, action) => {
};
}
if (type === 'backend/suspendStart') {
return {
...state,
suspending: true,
};
}
if (type === 'backend/suspendSuccess') {
const { timestamp } = payload;
return {
...state,
data: {},
shared: {},
config: {
...state.config,
title: '',
status: 1,
},
suspending: false,
suspended: timestamp,
};
}
return state;
};
export const backendMiddleware = store => {
let fancyState;
let suspendInterval;
return next => action => {
const { config, suspended } = selectBackend(store.getState());
const { type, payload } = action;
if (type === 'backend/suspendStart' && !suspendInterval) {
logger.log(`suspending (${window.__windowId__})`);
// Keep sending suspend messages until it succeeds.
// It may fail multiple times due to topic rate limiting.
const suspendFn = () => sendMessage({
type: 'suspend',
});
suspendFn();
suspendInterval = setInterval(suspendFn, 2000);
}
if (type === 'backend/suspendSuccess') {
clearInterval(suspendInterval);
suspendInterval = undefined;
releaseHeldKeys();
Byond.winset(window.__windowId__, {
'is-visible': false,
});
}
if (type === 'backend/update') {
const fancy = payload.config?.window?.fancy;
// Initialize fancy state
if (fancyState === undefined) {
fancyState = fancy;
}
// React to changes in fancy
else if (fancyState !== fancy) {
logger.log('changing fancy mode to', fancy);
fancyState = fancy;
Byond.winset(window.__windowId__, {
titlebar: !fancy,
'can-resize': !fancy,
});
}
}
if (type === 'backend/update' && suspended) {
// We schedule this for the next tick here because resizing and unhiding
// during the same tick will flash with a white background.
setImmediate(() => {
perf.mark('resume/start');
// Doublecheck if we are not re-suspended.
const { suspended } = selectBackend(store.getState());
if (suspended) {
return;
}
Byond.winset(window.__windowId__, {
'is-visible': true,
});
perf.mark('resume/finish');
if (process.env.NODE_ENV !== 'production') {
logger.log('visible in',
perf.measure('render/finish', 'resume/finish'));
}
});
}
return next(action);
};
};
/**
* Sends a message to /datum/tgui_window.
*/
export const sendMessage = (message = {}) => {
const { payload, ...rest } = message;
const data = {
// Message identifying header
tgui: 1,
window_id: window.__windowId__,
// Message body
...rest,
};
// JSON-encode the payload
if (payload !== null && payload !== undefined) {
data.payload = JSON.stringify(payload);
}
Byond.topic(data);
};
/**
* Sends an action to `ui_act` on `src_object` that this tgui window
* is associated with.
*/
export const sendAct = (action, payload = {}) => {
// Validate that payload is an object
const isObject = typeof payload === 'object'
&& payload !== null
&& !Array.isArray(payload);
if (!isObject) {
logger.error(`Payload for act() must be an object, got this:`, payload);
return;
}
sendMessage({
type: 'act/' + action,
payload,
});
};
/**
* @typedef BackendState
* @type {{
* config: {
* title: string,
* status: number,
* screen: string,
* style: string,
* interface: string,
* fancy: number,
* locked: number,
* observer: number,
* window: string,
* ref: string,
* user: {
* name: string,
* ckey: string,
* observer: number,
* },
* window: {
* key: string,
* size: [number, number],
* fancy: boolean,
* locked: boolean,
* },
* },
* data: any,
* shared: any,
* visible: boolean,
* interactive: boolean,
* suspending: boolean,
* suspended: boolean,
* }}
*/
/**
* Selects a backend-related slice of Redux state
*
* @return {BackendState}
*/
export const selectBackend = state => state.backend || {};
/**
* A React hook (sort of) for getting tgui state and related functions.
*
@@ -108,21 +274,16 @@ export const backendReducer = (state, action) => {
* be used in functional components.
*
* @return {BackendState & {
* act: (action: string, params?: object) => void,
* act: sendAct,
* }}
*/
export const useBackend = context => {
const { store } = context;
const state = store.getState();
const ref = state.config.ref;
const act = (action, params = {}) => {
callByond('', {
src: ref,
action,
...params,
});
const state = selectBackend(store.getState());
return {
...state,
act: sendAct,
};
return { ...state, act };
};
/**
@@ -140,7 +301,7 @@ export const useBackend = context => {
*/
export const useLocalState = (context, key, initialState) => {
const { store } = context;
const state = store.getState();
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
@@ -148,7 +309,11 @@ export const useLocalState = (context, key, initialState) => {
return [
sharedState,
nextState => {
store.dispatch(backendSetSharedState(key, nextState));
store.dispatch(backendSetSharedState(key, (
typeof nextState === 'function'
? nextState(sharedState)
: nextState
)));
},
];
};
@@ -169,8 +334,7 @@ export const useLocalState = (context, key, initialState) => {
*/
export const useSharedState = (context, key, initialState) => {
const { store } = context;
const state = store.getState();
const ref = state.config.ref;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
@@ -178,11 +342,14 @@ export const useSharedState = (context, key, initialState) => {
return [
sharedState,
nextState => {
callByond('', {
src: ref,
action: 'tgui:setSharedState',
sendMessage({
type: 'setSharedState',
key,
value: JSON.stringify(nextState) || '',
value: JSON.stringify(
typeof nextState === 'function'
? nextState(sharedState)
: nextState
) || '',
});
},
];
+17 -6
View File
@@ -9,17 +9,22 @@ import { createVNode } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
const UNIT_PX = 12;
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = value => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
return parseFloat(value) / 12 + 'rem';
}
return value;
}
if (typeof value === 'number') {
return (value * UNIT_PX) + 'px';
if (Byond.IS_LTE_IE8) {
return value * 12 + 'px';
}
return value + 'rem';
}
};
@@ -28,10 +33,10 @@ export const unit = value => {
*/
export const halfUnit = value => {
if (typeof value === 'string') {
return value;
return unit(value);
}
if (typeof value === 'number') {
return (value * UNIT_PX * 0.5) + 'px';
return unit(value * 0.5);
}
};
@@ -90,7 +95,13 @@ const styleMapperByPropName = {
maxHeight: mapUnitPropTo('max-height', unit),
fontSize: mapUnitPropTo('font-size', unit),
fontFamily: mapRawPropTo('font-family'),
lineHeight: mapRawPropTo('line-height'),
lineHeight: (style, value) => {
if (!isFalsy(value)) {
style['line-height'] = typeof value === 'number'
? value
: unit(value);
}
},
opacity: mapRawPropTo('opacity'),
textAlign: mapRawPropTo('text-align'),
verticalAlign: mapRawPropTo('vertical-align'),
+5 -3
View File
@@ -6,7 +6,6 @@
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { IS_IE8 } from '../byond';
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from '../hotkeys';
import { refocusLayout } from '../layouts';
import { createLogger } from '../logging';
@@ -61,7 +60,7 @@ export const Button = props => {
className,
])}
tabIndex={!disabled && '0'}
unselectable={IS_IE8}
unselectable={Byond.IS_LTE_IE8}
onclick={e => {
refocusLayout();
if (!disabled && onClick) {
@@ -87,7 +86,10 @@ export const Button = props => {
}}
{...rest}>
{icon && (
<Icon name={icon} rotation={iconRotation} spin={iconSpin} />
<Icon
name={icon}
rotation={iconRotation}
spin={iconSpin} />
)}
{content}
{children}
+13 -27
View File
@@ -7,7 +7,6 @@
import { shallowDiffers } from 'common/react';
import { debounce } from 'common/timer';
import { Component, createRef } from 'inferno';
import { callByond, IS_IE8 } from '../byond';
import { createLogger } from '../logging';
import { computeBoxProps } from './Box';
@@ -28,16 +27,12 @@ const createByondUiElement = elementId => {
render: params => {
logger.log(`rendering '${id}'`);
byondUiStack[index] = id;
callByond('winset', {
...params,
id,
});
Byond.winset(id, params);
},
unmount: () => {
logger.log(`unmounting '${id}'`);
byondUiStack[index] = null;
callByond('winset', {
id,
Byond.winset(id, {
parent: '',
});
},
@@ -51,8 +46,7 @@ window.addEventListener('beforeunload', () => {
if (typeof id === 'string') {
logger.log(`unmounting '${id}' (beforeunload)`);
byondUiStack[index] = null;
callByond('winset', {
id,
Byond.winset(id, {
parent: '',
});
}
@@ -83,7 +77,7 @@ export class ByondUi extends Component {
this.byondUiElement = createByondUiElement(props.params?.id);
this.handleResize = debounce(() => {
this.forceUpdate();
}, 500);
}, 100);
}
shouldComponentUpdate(nextProps) {
@@ -101,16 +95,17 @@ export class ByondUi extends Component {
componentDidMount() {
// IE8: It probably works, but fuck you anyway.
if (IS_IE8) {
if (Byond.IS_LTE_IE10) {
return;
}
window.addEventListener('resize', this.handleResize);
return this.componentDidUpdate();
this.componentDidUpdate();
this.handleResize();
}
componentDidUpdate() {
// IE8: It probably works, but fuck you anyway.
if (IS_IE8) {
if (Byond.IS_LTE_IE10) {
return;
}
const {
@@ -119,6 +114,7 @@ export class ByondUi extends Component {
const box = getBoundingBox(this.containerRef.current);
logger.log('bounding box', box);
this.byondUiElement.render({
parent: window.__windowId__,
...params,
pos: box.pos[0] + ',' + box.pos[1],
size: box.size[0] + 'x' + box.size[1],
@@ -127,7 +123,7 @@ export class ByondUi extends Component {
componentWillUnmount() {
// IE8: It probably works, but fuck you anyway.
if (IS_IE8) {
if (Byond.IS_LTE_IE10) {
return;
}
window.removeEventListener('resize', this.handleResize);
@@ -135,26 +131,16 @@ export class ByondUi extends Component {
}
render() {
const {
parent,
params,
...rest
} = this.props;
const { params, ...rest } = this.props;
const type = params?.type;
const boxProps = computeBoxProps(rest);
return (
<div
ref={this.containerRef}
{...boxProps}>
{type === 'button' && <ButtonMock />}
{/* Filler */}
<div style={{ 'min-height': '22px' }} />
</div>
);
}
}
const ButtonMock = () => (
<div
style={{
'min-height': '22px',
}} />
);
+1 -2
View File
@@ -7,7 +7,6 @@
import { map, zipWith } from 'common/collections';
import { pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { IS_IE8 } from '../byond';
import { Box } from './Box';
const normalizeData = (data, scale, rangeX, rangeY) => {
@@ -123,5 +122,5 @@ const Stub = props => null;
// IE8: No inline svg support
export const Chart = {
Line: IS_IE8 ? Stub : LineChart,
Line: Byond.IS_LTE_IE8 ? Stub : LineChart,
};
+4 -5
View File
@@ -5,7 +5,6 @@
*/
import { classes, pureComponentHooks } from 'common/react';
import { IS_IE8 } from '../byond';
import { Box, unit } from './Box';
export const computeFlexProps = props => {
@@ -22,10 +21,10 @@ export const computeFlexProps = props => {
return {
className: classes([
'Flex',
IS_IE8 && (
Byond.IS_LTE_IE10 && (
direction === 'column'
? 'Flex--ie8--column'
: 'Flex--ie8'
? 'Flex--iefix--column'
: 'Flex--iefix'
),
inline && 'Flex--inline',
spacing > 0 && 'Flex--spacing--' + spacing,
@@ -63,7 +62,7 @@ export const computeFlexItemProps = props => {
return {
className: classes([
'Flex__item',
IS_IE8 && 'Flex__item--ie8',
Byond.IS_LTE_IE10 && 'Flex__item--iefix',
className,
]),
style: {
+4
View File
@@ -83,6 +83,10 @@ export class Input extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
if (this.props.autoFocus) {
setTimeout(() => input.focus(), 1);
}
}
componentDidUpdate(prevProps, prevState) {
+1 -2
View File
@@ -6,7 +6,6 @@
import { keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { IS_IE8 } from '../byond';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
@@ -14,7 +13,7 @@ import { NumberInput } from './NumberInput';
export const Knob = props => {
// IE8: I don't want to support a yet another component on IE8.
// IE8: It also can't handle SVG.
if (IS_IE8) {
if (Byond.IS_LTE_IE8) {
return (
<NumberInput {...props} />
);
+1 -2
View File
@@ -7,7 +7,6 @@
import { clamp } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { IS_IE8 } from '../byond';
import { AnimatedNumber } from './AnimatedNumber';
import { Box } from './Box';
@@ -168,7 +167,7 @@ export class NumberInput extends Component {
const renderContentElement = value => (
<div
className="NumberInput__content"
unselectable={IS_IE8}>
unselectable={Byond.IS_LTE_IE8}>
{value + (unit ? ' ' + unit : '')}
</div>
);
+8 -7
View File
@@ -5,7 +5,7 @@
*/
import { classes, isFalsy, pureComponentHooks } from 'common/react';
import { Box } from './Box';
import { computeBoxClassName, computeBoxProps } from './Box';
export const Section = props => {
const {
@@ -13,20 +13,22 @@ export const Section = props => {
title,
level = 1,
buttons,
content,
fill,
children,
...rest
} = props;
const hasTitle = !isFalsy(title) || !isFalsy(buttons);
const hasContent = !isFalsy(content) || !isFalsy(children);
const hasContent = !isFalsy(children);
return (
<Box
<div
className={classes([
'Section',
'Section--level--' + level,
fill && 'Section--fill',
className,
...computeBoxClassName(rest),
])}
{...rest}>
{...computeBoxProps(rest)}>
{hasTitle && (
<div className="Section__title">
<span className="Section__titleText">
@@ -39,11 +41,10 @@ export const Section = props => {
)}
{hasContent && (
<div className="Section__content">
{content}
{children}
</div>
)}
</Box>
</div>
);
};
+1 -2
View File
@@ -6,14 +6,13 @@
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { IS_IE8 } from '../byond';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
export const Slider = props => {
// IE8: I don't want to support a yet another component on IE8.
if (IS_IE8) {
if (Byond.IS_LTE_IE8) {
return (
<NumberInput {...props} />
);
+2 -1
View File
@@ -14,6 +14,7 @@ export { Collapsible } from './Collapsible';
export { ColorBox } from './ColorBox';
export { Dimmer } from './Dimmer';
export { Divider } from './Divider';
export { DraggableControl } from './DraggableControl';
export { Dropdown } from './Dropdown';
export { Flex } from './Flex';
export { Grid } from './Grid';
@@ -29,7 +30,7 @@ export { ProgressBar } from './ProgressBar';
export { Section } from './Section';
export { Slider } from './Slider';
export { Table } from './Table';
export { TextArea } from './TextArea';
export { Tabs } from './Tabs';
export { Tooltip } from './Tooltip';
export { TimeDisplay } from './TimeDisplay';
// export { TextArea } from './TextArea'; PAPER
+608
View File
@@ -0,0 +1,608 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Box, Button, ByondUi, Collapsible, DraggableControl, Flex, Icon, Input, Knob, LabeledList, NoticeBox, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
import { formatSiUnit } from '../format';
import { Window } from '../layouts';
import { createLogger } from '../logging';
const logger = createLogger('KitchenSink');
const COLORS_SPECTRUM = [
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
];
const COLORS_STATES = [
'good',
'average',
'bad',
'black',
'white',
];
const PAGES = [
{
title: 'Button',
component: () => KitchenSinkButton,
},
{
title: 'Box',
component: () => KitchenSinkBox,
},
{
title: 'Flex & Sections',
component: () => KitchenSinkFlexAndSections,
},
{
title: 'ProgressBar',
component: () => KitchenSinkProgressBar,
},
{
title: 'Tabs',
component: () => KitchenSinkTabs,
},
{
title: 'Tooltip',
component: () => KitchenSinkTooltip,
},
{
title: 'Input / Control',
component: () => KitchenSinkInput,
},
{
title: 'Collapsible',
component: () => KitchenSinkCollapsible,
},
{
title: 'BlockQuote',
component: () => KitchenSinkBlockQuote,
},
{
title: 'ByondUi',
component: () => KitchenSinkByondUi,
},
{
title: 'Themes',
component: () => KitchenSinkThemes,
},
{
title: 'Storage',
component: () => KitchenSinkStorage,
},
];
export const KitchenSink = (props, context) => {
const [theme] = useLocalState(context, 'kitchenSinkTheme');
const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
const PageComponent = PAGES[pageIndex].component();
return (
<Window
title="Kitchen Sink"
width={600}
height={500}
theme={theme}
resizable>
<Flex height="100%">
<Flex.Item m={1} mr={0}>
<Section fill>
{PAGES.map((page, i) => (
<Button
key={i}
fluid
color="transparent"
selected={i === pageIndex}
onClick={() => setPageIndex(i)}>
{page.title}
</Button>
))}
</Section>
</Flex.Item>
<Flex.Item
position="relative"
grow={1}>
<Window.Content scrollable>
<PageComponent />
</Window.Content>
</Flex.Item>
</Flex>
</Window>
);
};
const KitchenSinkButton = props => {
return (
<Section>
<Box mb={1}>
<Button content="Simple" />
<Button selected content="Selected" />
<Button altSelected content="Alt Selected" />
<Button disabled content="Disabled" />
<Button color="transparent" content="Transparent" />
<Button icon="cog" content="Icon" />
<Button icon="power-off" />
<Button fluid content="Fluid" />
<Button
my={1}
lineHeight={2}
minWidth={15}
textAlign="center"
content="With Box props" />
</Box>
<Box mb={1}>
{COLORS_STATES.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Box inline
mx="7px"
key={color}
color={color}>
{color}
</Box>
))}
</Box>
</Section>
);
};
const KitchenSinkBox = props => {
return (
<Section>
<Box bold>
bold
</Box>
<Box italic>
italic
</Box>
<Box opacity={0.5}>
opacity 0.5
</Box>
<Box opacity={0.25}>
opacity 0.25
</Box>
<Box m={2}>
m: 2
</Box>
<Box textAlign="left">
left
</Box>
<Box textAlign="center">
center
</Box>
<Box textAlign="right">
right
</Box>
</Section>
);
};
const KitchenSinkFlexAndSections = (props, context) => {
const [grow, setGrow] = useLocalState(
context, 'fs_grow', 1);
const [direction, setDirection] = useLocalState(
context, 'fs_direction', 'column');
const [fill, setFill] = useLocalState(
context, 'fs_fill', true);
const [hasTitle, setHasTitle] = useLocalState(
context, 'fs_title', true);
return (
<Flex
height="100%"
direction="column">
<Flex.Item mb={1}>
<Section>
<Button
fluid
onClick={() => setDirection(
direction === 'column' ? 'row' : 'column'
)}>
{`Flex direction="${direction}"`}
</Button>
<Button
fluid
onClick={() => setGrow(Number(!grow))}>
{`Flex.Item grow={${grow}}`}
</Button>
<Button
fluid
onClick={() => setFill(!fill)}>
{`Section fill={${String(fill)}}`}
</Button>
<Button
fluid
selected={hasTitle}
onClick={() => setHasTitle(!hasTitle)}>
{`Section title`}
</Button>
</Section>
</Flex.Item>
<Flex.Item grow={1}>
<Flex
height="100%"
direction={direction}>
<Flex.Item
mr={direction === 'row' && 1}
mb={direction === 'column' && 1}
grow={grow}>
<Section
title={hasTitle && "Section 1"}
fill={fill}>
Content
</Section>
</Flex.Item>
<Flex.Item grow={grow}>
<Section
title={hasTitle && "Section 2"}
fill={fill}>
Content
</Section>
</Flex.Item>
</Flex>
</Flex.Item>
</Flex>
);
};
const KitchenSinkProgressBar = (props, context) => {
const [
progress,
setProgress,
] = useLocalState(context, 'progress', 0.5);
return (
<Section>
<ProgressBar
ranges={{
good: [0.5, Infinity],
bad: [-Infinity, 0.1],
average: [0, 0.5],
}}
minValue={-1}
maxValue={1}
value={progress}>
Value: {Number(progress).toFixed(1)}
</ProgressBar>
<Box mt={1}>
<Button
content="-0.1"
onClick={() => setProgress(progress - 0.1)} />
<Button
content="+0.1"
onClick={() => setProgress(progress + 0.1)} />
</Box>
</Section>
);
};
const KitchenSinkTabs = (props, context) => {
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [vertical, setVertical] = useLocalState(context, 'tabVert');
const [altSelection, setAltSelection] = useLocalState(context, 'tabAlt');
const TAB_RANGE = [1, 2, 3, 4, 5];
return (
<Section>
<Box mb={2}>
<Button.Checkbox
inline
content="vertical"
checked={vertical}
onClick={() => setVertical(!vertical)} />
<Button.Checkbox
inline
content="altSelection"
checked={altSelection}
onClick={() => setAltSelection(!altSelection)} />
</Box>
<Tabs vertical={vertical}>
{TAB_RANGE.map((number, i) => (
<Tabs.Tab
key={i}
altSelection={altSelection}
selected={i === tabIndex}
onClick={() => setTabIndex(i)}>
Tab #{number}
</Tabs.Tab>
))}
</Tabs>
</Section>
);
};
const KitchenSinkTooltip = props => {
const positions = [
'top',
'left',
'right',
'bottom',
'bottom-left',
'bottom-right',
];
return (
<Section>
<Box>
<Box inline position="relative" mr={1}>
Box (hover me).
<Tooltip content="Tooltip text." />
</Box>
<Button
tooltip="Tooltip text."
content="Button" />
</Box>
<Box mt={1}>
{positions.map(position => (
<Button
key={position}
color="transparent"
tooltip="Tooltip text."
tooltipPosition={position}
content={position} />
))}
</Box>
</Section>
);
};
const KitchenSinkInput = (props, context) => {
const [
number,
setNumber,
] = useLocalState(context, 'number', 0);
const [
text,
setText,
] = useLocalState(context, 'text', "Sample text");
return (
<Section>
<LabeledList>
<LabeledList.Item label="Input (onChange)">
<Input
value={text}
onChange={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="Input (onInput)">
<Input
value={text}
onInput={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onChange)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onChange={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onDrag)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="Slider (onDrag)">
<Slider
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="Knob (onDrag)">
<Knob
inline
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
<Knob
ml={1}
inline
bipolar
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="Rotating Icon">
<Box inline position="relative">
<DraggableControl
value={number}
minValue={-100}
maxValue={100}
dragMatrix={[0, -1]}
step={1}
stepPixelSize={5}
onDrag={(e, value) => setNumber(value)}>
{control => (
<Box onMouseDown={control.handleDragStart}>
<Icon
size={4}
color="yellow"
name="times"
rotation={control.displayValue * 4} />
{control.inputElement}
</Box>
)}
</DraggableControl>
</Box>
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const KitchenSinkCollapsible = props => {
return (
<Section>
<Collapsible
title="Collapsible Demo"
buttons={(
<Button icon="cog" />
)}>
<BoxWithSampleText />
</Collapsible>
</Section>
);
};
const BoxWithSampleText = props => {
return (
<Box {...props}>
<Box italic>
Jackdaws love my big sphinx of quartz.
</Box>
<Box mt={1} bold>
The wide electrification of the southern
provinces will give a powerful impetus to the
growth of agriculture.
</Box>
</Box>
);
};
const KitchenSinkBlockQuote = props => {
return (
<Section>
<BlockQuote>
<BoxWithSampleText />
</BlockQuote>
</Section>
);
};
const KitchenSinkByondUi = (props, context) => {
const { config } = useBackend(context);
const [code, setCode] = useLocalState(context,
'byondUiEvalCode',
`Byond.winset('${window.__windowId__}', {\n 'is-visible': true,\n})`);
return (
<Fragment>
<Section title="Button">
<ByondUi
params={{
type: 'button',
text: 'Button',
}} />
</Section>
<Section
title="Make BYOND calls"
buttons={(
<Button
icon="chevron-right"
onClick={() => setImmediate(() => {
try {
const result = new Function('return (' + code + ')')();
if (result && result.then) {
logger.log('Promise');
result.then(logger.log);
}
else {
logger.log(result);
}
}
catch (err) {
logger.log(err);
}
})}>
Evaluate
</Button>
)}>
<Box
as="textarea"
width="100%"
height="10em"
onChange={e => setCode(e.target.value)}>
{code}
</Box>
</Section>
</Fragment>
);
};
const KitchenSinkThemes = (props, context) => {
const [theme, setTheme] = useLocalState(context, 'kitchenSinkTheme');
return (
<Section>
<LabeledList>
<LabeledList.Item label="Use theme">
<Input
placeholder="theme_name"
value={theme}
onInput={(e, value) => setTheme(value)} />
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const KitchenSinkStorage = (props, context) => {
if (!window.localStorage) {
return (
<NoticeBox>
Local storage is not available.
</NoticeBox>
);
}
return (
<Section
title="Local Storage"
buttons={(
<Button
icon="recycle"
onClick={() => {
localStorage.clear();
}}>
Clear
</Button>
)}>
<LabeledList>
<LabeledList.Item label="Keys in use">
{localStorage.length}
</LabeledList.Item>
<LabeledList.Item label="Remaining space">
{formatSiUnit(localStorage.remainingSpace, 0, 'B')}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
+49
View File
@@ -0,0 +1,49 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { subscribeToHotKey } from '../hotkeys';
export const toggleKitchenSink = () => ({
type: 'debug/toggleKitchenSink',
});
export const toggleDebugLayout = () => ({
type: 'debug/toggleDebugLayout',
});
subscribeToHotKey('F11', () => toggleDebugLayout());
subscribeToHotKey('F12', () => toggleKitchenSink());
subscribeToHotKey('Ctrl+Alt+[8]', () => {
// NOTE: We need to call this in a timeout, because we need a clean
// stack in order for this to be a fatal error.
setTimeout(() => {
throw new Error(
'OOPSIE WOOPSIE!! UwU We made a fucky wucky!! A wittle'
+ ' fucko boingo! The code monkeys at our headquarters are'
+ ' working VEWY HAWD to fix this!');
});
});
export const selectDebug = state => state.debug;
export const useDebug = context => selectDebug(context.store.getState());
export const debugReducer = (state = {}, action) => {
const { type, payload } = action;
if (type === 'debug/toggleKitchenSink') {
return {
...state,
kitchenSink: !state.kitchenSink,
};
}
if (type === 'debug/toggleDebugLayout') {
return {
...state,
debugLayout: !state.debugLayout,
};
}
return state;
};
+144 -51
View File
@@ -4,79 +4,171 @@
* @license MIT
*/
import { vecAdd, vecInverse, vecMultiply } from 'common/vector';
import { winget, winset } from './byond';
import { storage } from 'common/storage';
import { vecAdd, vecInverse, vecMultiply, vecScale } from 'common/vector';
import { createLogger } from './logging';
const logger = createLogger('drag');
let ref;
let windowKey = window.__windowId__;
let dragging = false;
let resizing = false;
let screenOffset = [0, 0];
let screenOffsetPromise;
let dragPointOffset;
let resizeMatrix;
let initialSize;
let size;
const getWindowPosition = ref => {
return winget(ref, 'pos').then(pos => [pos.x, pos.y]);
export const setWindowKey = key => {
windowKey = key;
};
const setWindowPosition = (ref, vec) => {
return winset(ref, 'pos', vec[0] + ',' + vec[1]);
export const getWindowPosition = () => [
window.screenLeft,
window.screenTop,
];
export const getWindowSize = () => [
window.innerWidth,
window.innerHeight,
];
export const setWindowPosition = vec => {
const byondPos = vecAdd(vec, screenOffset);
return Byond.winset(window.__windowId__, {
pos: byondPos[0] + ',' + byondPos[1],
});
};
const setWindowSize = (ref, vec) => {
return winset(ref, 'size', vec[0] + ',' + vec[1]);
export const setWindowSize = vec => {
return Byond.winset(window.__windowId__, {
size: vec[0] + 'x' + vec[1],
});
};
export const setupDrag = async state => {
logger.log('setting up');
ref = state.config.window;
// Calculate offset caused by windows taskbar
const realPosition = await getWindowPosition(ref);
screenOffset = [
realPosition[0] - window.screenLeft,
realPosition[1] - window.screenTop,
];
// Constraint window position
const [relocated, safePosition] = constraintPosition(realPosition);
if (relocated) {
setWindowPosition(ref, safePosition);
export const getScreenPosition = () => [
0 - screenOffset[0],
0 - screenOffset[1],
];
export const getScreenSize = () => [
window.screen.availWidth,
window.screen.availHeight,
];
/**
* Moves an item to the top of the recents array, and keeps its length
* limited to the number in `limit` argument.
*
* Uses a strict equality check for comparisons.
*
* Returns new recents and an item which was trimmed.
*/
const touchRecents = (recents, touchedItem, limit = 50) => {
const nextRecents = [touchedItem];
let trimmedItem;
for (let i = 0; i < recents.length; i++) {
const item = recents[i];
if (item === touchedItem) {
continue;
}
if (nextRecents.length < limit) {
nextRecents.push(item);
}
else {
trimmedItem = item;
}
}
logger.debug('current state', { ref, screenOffset });
return [nextRecents, trimmedItem];
};
export const storeWindowGeometry = windowKey => {
logger.log('storing geometry');
const geometry = {
pos: getWindowPosition(),
size: getWindowSize(),
};
storage.set(windowKey, geometry);
// Update the list of stored geometries
const [geometries, trimmedKey] = touchRecents(
storage.get('geometries') || [],
windowKey);
if (trimmedKey) {
storage.remove(trimmedKey);
}
storage.set('geometries', geometries);
};
export const recallWindowGeometry = async (windowKey, options = {}) => {
// Only recall geometry in fancy mode
const geometry = options.fancy && storage.get(windowKey);
if (geometry) {
logger.log('recalled geometry:', geometry);
}
let pos = geometry?.pos || options.pos;
const size = options.size;
// Set window size
if (size) {
setWindowSize(size);
}
// Set window position
if (pos) {
await screenOffsetPromise;
// Constraint window position if monitor lock was set in preferences.
if (size && options.locked) {
pos = constraintPosition(pos, size)[1];
}
setWindowPosition(pos);
}
// Set window position at the center of the screen.
else if (size) {
await screenOffsetPromise;
const areaAvailable = [
window.screen.availWidth - Math.abs(screenOffset[0]),
window.screen.availHeight - Math.abs(screenOffset[1]),
];
const pos = vecAdd(
vecScale(areaAvailable, 0.5),
vecScale(size, -0.5),
vecScale(screenOffset, -1.0));
setWindowPosition(pos);
}
};
export const setupDrag = async () => {
// Calculate screen offset caused by the windows taskbar
screenOffsetPromise = Byond.winget(window.__windowId__, 'pos')
.then(pos => [
pos.x - window.screenLeft,
pos.y - window.screenTop,
]);
screenOffset = await screenOffsetPromise;
logger.debug('screen offset', screenOffset);
};
/**
* Constraints window position to safe screen area, accounting for safe
* margins which could be a system taskbar.
*/
const constraintPosition = position => {
let x = position[0];
let y = position[1];
const constraintPosition = (pos, size) => {
const screenPos = getScreenPosition();
const screenSize = getScreenSize();
const nextPos = [pos[0], pos[1]];
let relocated = false;
// Left
if (x < 0) {
x = 0;
relocated = true;
for (let i = 0; i < 2; i++) {
const leftBoundary = screenPos[i];
const rightBoundary = screenPos[i] + screenSize[i];
if (pos[i] < leftBoundary) {
nextPos[i] = leftBoundary;
relocated = true;
}
else if (pos[i] + size[i] > rightBoundary) {
nextPos[i] = rightBoundary - size[i];
relocated = true;
}
}
// Right
else if (x + window.innerWidth > window.screen.availWidth) {
x = window.screen.availWidth - window.innerWidth;
relocated = true;
}
// Top
if (y < 0) {
y = 0;
relocated = true;
}
// Bottom
else if (y + window.innerHeight > window.screen.availHeight) {
y = window.screen.availHeight - window.innerHeight;
relocated = true;
}
return [relocated, [x, y]];
return [relocated, nextPos];
};
export const dragStartHandler = event => {
@@ -97,6 +189,7 @@ const dragEndHandler = event => {
document.removeEventListener('mousemove', dragMoveHandler);
document.removeEventListener('mouseup', dragEndHandler);
dragging = false;
storeWindowGeometry(windowKey);
};
const dragMoveHandler = event => {
@@ -104,9 +197,8 @@ const dragMoveHandler = event => {
return;
}
event.preventDefault();
setWindowPosition(ref, vecAdd(
setWindowPosition(vecAdd(
[event.screenX, event.screenY],
screenOffset,
dragPointOffset));
};
@@ -133,6 +225,7 @@ const resizeEndHandler = event => {
document.removeEventListener('mousemove', resizeMoveHandler);
document.removeEventListener('mouseup', resizeEndHandler);
resizing = false;
storeWindowGeometry(windowKey);
};
const resizeMoveHandler = event => {
@@ -146,7 +239,7 @@ const resizeMoveHandler = event => {
dragPointOffset,
[1, 1])));
// Sane window size values
size[0] = Math.max(size[0], 250);
size[1] = Math.max(size[1], 120);
setWindowSize(ref, size);
size[0] = Math.max(size[0], 150);
size[1] = Math.max(size[1], 50);
setWindowSize(size);
};
+3
View File
@@ -39,6 +39,9 @@ export const formatSiUnit = (
minBase1000 = -SI_BASE_INDEX,
unit = ''
) => {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return value;
}
const realBase10 = Math.floor(Math.log10(value));
const base10 = Math.floor(Math.max(minBase1000 * 3, realBase10));
const realBase1000 = Math.floor(realBase10 / 3);
+95
View File
@@ -0,0 +1,95 @@
interface ByondType {
/**
* True if javascript is running in BYOND.
*/
IS_BYOND: boolean;
/**
* True if browser is IE8 or lower.
*/
IS_LTE_IE8: boolean;
/**
* True if browser is IE9 or lower.
*/
IS_LTE_IE9: boolean;
/**
* True if browser is IE10 or lower.
*/
IS_LTE_IE10: boolean;
/**
* True if browser is IE11 or lower.
*/
IS_LTE_IE11: boolean;
/**
* Makes a BYOND call.
*
* If path is empty, this will trigger a Topic call.
* You can reference a specific object by setting the "src" parameter.
*
* See: https://secure.byond.com/docs/ref/skinparams.html
*/
call(path: string, params: object): void;
/**
* Makes an asynchronous BYOND call. Returns a promise.
*/
callAsync(path: string, params: object): Promise<any>;
/**
* Makes a Topic call.
*
* You can reference a specific object by setting the "src" parameter.
*/
topic(params: object): void;
/**
* Runs a command or a verb.
*/
command(command: string): void;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string): Promise<object>;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string, propName: '*'): Promise<object>;
/**
* Retrieves an exactly one property of the BYOND skin element,
* as defined in `propName`.
*
* Returns a promise with the value of that property.
*/
winget(id: string, propName: string): Promise<any>;
/**
* Retrieves multiple properties of the BYOND skin element,
* as listen in the `propNames` array.
*
* Returns a promise with a key-value object containing listed properties.
*/
winget(id: string, propNames: string[]): Promise<object>;
/**
* Assigns properties to the BYOND skin element.
*/
winset(id: string, props: object): void;
/**
* Sets a property on the BYOND skin element to a certain value.
*/
winset(id: string, propName: string, propValue: any): void;
};
declare const Byond: ByondType;
+55 -50
View File
@@ -4,7 +4,6 @@
* @license MIT
*/
import { callByond, IS_IE8 } from './byond';
import { createLogger } from './logging';
const logger = createLogger('hotkeys');
@@ -54,6 +53,18 @@ export const KEY_W = 87;
export const KEY_X = 88;
export const KEY_Y = 89;
export const KEY_Z = 90;
export const KEY_F1 = 112;
export const KEY_F2 = 113;
export const KEY_F3 = 114;
export const KEY_F4 = 115;
export const KEY_F5 = 116;
export const KEY_F6 = 117;
export const KEY_F7 = 118;
export const KEY_F8 = 119;
export const KEY_F9 = 120;
export const KEY_F10 = 121;
export const KEY_F11 = 122;
export const KEY_F12 = 123;
export const KEY_EQUAL = 187;
export const KEY_MINUS = 189;
@@ -70,6 +81,18 @@ const NO_PASSTHROUGH_KEYS = [
KEY_TAB,
KEY_CTRL,
KEY_SHIFT,
KEY_F1,
KEY_F2,
KEY_F3,
KEY_F4,
KEY_F5,
KEY_F6,
KEY_F7,
KEY_F8,
KEY_F9,
KEY_F10,
KEY_F11,
KEY_F12,
];
// Tracks the "pressed" state of keys
@@ -89,6 +112,9 @@ const createHotkeyString = (ctrlKey, altKey, shiftKey, keyCode) => {
if (keyCode >= 48 && keyCode <= 90) {
str += String.fromCharCode(keyCode);
}
else if (keyCode >= KEY_F1 && keyCode <= KEY_F12) {
str += 'F' + (keyCode - 111);
}
else {
str += '[' + keyCode + ']';
}
@@ -135,11 +161,11 @@ const handlePassthrough = (e, eventType) => {
// Send this keypress to BYOND
if (eventType === 'keydown' && !keyState[keyCode]) {
logger.debug('passthrough', eventType, keyData);
return callByond('', { __keydown: keyCode });
return Byond.topic({ __keydown: keyCode });
}
if (eventType === 'keyup' && keyState[keyCode]) {
logger.debug('passthrough', eventType, keyData);
return callByond('', { __keyup: keyCode });
return Byond.topic({ __keyup: keyCode });
}
};
@@ -152,41 +178,45 @@ export const releaseHeldKeys = () => {
if (keyState[keyCode]) {
logger.log(`releasing [${keyCode}] key`);
keyState[keyCode] = false;
callByond('', { __keyup: keyCode });
Byond.topic({ __keyup: keyCode });
}
}
};
const handleHotKey = (e, eventType, dispatch) => {
const hotKeySubscribers = [];
/**
* Subscribes to a certain hotkey, and dispatches a redux action returned
* by the callback function.
*/
export const subscribeToHotKey = (keyString, fn) => {
hotKeySubscribers.push((store, keyData) => {
if (keyData.keyString === keyString) {
const action = fn(store);
if (action) {
store.dispatch(action);
}
}
});
};
const handleHotKey = (e, eventType, store) => {
if (eventType !== 'keyup') {
return;
}
const keyData = getKeyData(e);
const {
ctrlKey,
altKey,
keyCode,
hasModifierKeys,
keyString,
} = keyData;
// Dispatch a detected hotkey as a store action
if (hasModifierKeys && !MODIFIER_KEYS.includes(keyCode)) {
if (hasModifierKeys && !MODIFIER_KEYS.includes(keyCode)
|| keyCode >= KEY_F1 && keyCode <= KEY_F12) {
logger.log(keyString);
// Fun stuff
if (ctrlKey && altKey && keyCode === KEY_BACKSPACE) {
// NOTE: We need to call this in a timeout, because we need a clean
// stack in order for this to be a fatal error.
setTimeout(() => {
throw new Error(
'OOPSIE WOOPSIE!! UwU We made a fucky wucky!! A wittle'
+ ' fucko boingo! The code monkeys at our headquarters are'
+ ' working VEWY HAWD to fix this!');
});
for (let subscriberFn of hotKeySubscribers) {
subscriberFn(store, keyData);
}
dispatch({
type: 'hotKey',
payload: keyData,
});
}
};
@@ -224,18 +254,17 @@ const subscribeToKeyPresses = listenerFn => {
// Middleware
export const hotKeyMiddleware = store => {
const { dispatch } = store;
// Subscribe to key events
subscribeToKeyPresses((e, eventType) => {
// IE8: Can't determine the focused element, so by extension it passes
// keypresses when inputs are focused.
if (!IS_IE8) {
if (!Byond.IS_LTE_IE8) {
handlePassthrough(e, eventType);
}
handleHotKey(e, eventType, dispatch);
handleHotKey(e, eventType, store);
});
// IE8: focusin/focusout only available on IE9+
if (!IS_IE8) {
if (!Byond.IS_LTE_IE8) {
// Clean up when browser window completely loses focus
subscribeToLossOfFocus(() => {
releaseHeldKeys();
@@ -244,27 +273,3 @@ export const hotKeyMiddleware = store => {
// Pass through store actions (do nothing)
return next => action => next(action);
};
// Reducer
export const hotKeyReducer = (state, action) => {
const { type, payload } = action;
if (type === 'hotKey') {
const { ctrlKey, altKey, keyCode } = payload;
// Toggle kitchen sink mode
if (ctrlKey && altKey && keyCode === KEY_EQUAL) {
return {
...state,
showKitchenSink: !state.showKitchenSink,
};
}
// Toggle layout debugger
if (ctrlKey && altKey && keyCode === KEY_MINUS) {
return {
...state,
debugLayout: !state.debugLayout,
};
}
return state;
}
return state;
};
+96 -62
View File
@@ -18,78 +18,79 @@ import './polyfills/inferno';
// Themes
import './styles/main.scss';
import './styles/themes/abductor.scss';
import './styles/themes/cardtable.scss';
import './styles/themes/hackerman.scss';
import './styles/themes/malfunction.scss';
import './styles/themes/ntos.scss';
import './styles/themes/hackerman.scss';
import './styles/themes/paper.scss';
import './styles/themes/retro.scss';
import './styles/themes/syndicate.scss';
import './styles/themes/clockcult.scss';
// import './styles/themes/paper.scss'; PAPER
// "marked": "^1.1.0", PAPER - package.json
// "dompurify": "^2.0.11"
import { loadCSS } from 'fg-loadcss';
import { perf } from 'common/perf';
import { render } from 'inferno';
import { setupHotReloading } from 'tgui-dev-server/link/client';
import { backendUpdate } from './backend';
import { IS_IE8 } from './byond';
import { backendUpdate, backendSuspendSuccess, selectBackend, sendMessage } from './backend';
import { setupDrag } from './drag';
import { logger } from './logging';
import { createStore, StoreProvider } from './store';
const enteredBundleAt = Date.now();
perf.mark('inception', window.__inception__);
perf.mark('init');
const store = createStore();
let reactRoot;
let initialRender = true;
const renderLayout = () => {
// Mark the beginning of the render
let startedAt;
if (process.env.NODE_ENV !== 'production') {
startedAt = Date.now();
}
try {
const state = store.getState();
// Initial render setup
if (initialRender) {
logger.log('initial render', state);
// Setup dragging
setupDrag(state);
perf.mark('render/start');
const state = store.getState();
const { suspended, assets } = selectBackend(state);
// Initial render setup
if (initialRender) {
logger.log('initial render', state);
// Setup dragging
if (initialRender !== 'recycled') {
setupDrag();
}
// Start rendering
const { getRoutedComponent } = require('./routes');
const Component = getRoutedComponent(state);
const element = (
<StoreProvider store={store}>
<Component />
</StoreProvider>
);
if (!reactRoot) {
reactRoot = document.getElementById('react-root');
}
render(element, reactRoot);
}
catch (err) {
logger.error('rendering error', err);
throw err;
// Start rendering
const { getRoutedComponent } = require('./routes');
const Component = getRoutedComponent(state);
const element = (
<StoreProvider store={store}>
<Component />
</StoreProvider>
);
if (!reactRoot) {
reactRoot = document.getElementById('react-root');
}
render(element, reactRoot);
if (suspended) {
return;
}
perf.mark('render/finish');
// Report rendering time
if (process.env.NODE_ENV !== 'production') {
const finishedAt = Date.now();
if (initialRender) {
if (initialRender === 'recycled') {
logger.log('rendered in',
perf.measure('render/start', 'render/finish'));
}
else if (initialRender) {
logger.debug('serving from:', location.href);
logger.debug('bundle entered in', timeDiff(
window.__inception__, enteredBundleAt));
logger.debug('initialized in', timeDiff(
enteredBundleAt, startedAt));
logger.log('rendered in', timeDiff(
startedAt, finishedAt));
logger.log('fully loaded in', timeDiff(
window.__inception__, finishedAt));
logger.debug('bundle entered in',
perf.measure('inception', 'init'));
logger.debug('initialized in',
perf.measure('init', 'render/start'));
logger.log('rendered in',
perf.measure('render/start', 'render/finish'));
logger.log('fully loaded in',
perf.measure('inception', 'render/finish'));
}
else {
logger.debug('rendered in', timeDiff(startedAt, finishedAt));
logger.debug('rendered in',
perf.measure('render/start', 'render/finish'));
}
}
if (initialRender) {
@@ -97,12 +98,6 @@ const renderLayout = () => {
}
};
const timeDiff = (startedAt, finishedAt) => {
const diff = finishedAt - startedAt;
const diffFrames = (diff / 16.6667).toFixed(2);
return `${diff}ms (${diffFrames} frames)`;
};
// Parse JSON and report all abnormal JSON strings coming from BYOND
const parseStateJson = json => {
let reviver = (key, value) => {
@@ -115,7 +110,7 @@ const parseStateJson = json => {
};
// IE8: No reviver for you!
// See: https://stackoverflow.com/questions/1288962
if (IS_IE8) {
if (Byond.IS_LTE_IE8) {
reviver = undefined;
}
try {
@@ -136,14 +131,36 @@ const setupApp = () => {
});
// Subscribe for bankend updates
window.update = stateJson => {
// NOTE: stateJson can be an object only if called manually from console.
window.update = messageJson => {
const { suspended } = selectBackend(store.getState());
// NOTE: messageJson can be an object only if called manually from console.
// This is useful for debugging tgui in external browsers, like Chrome.
const state = typeof stateJson === 'string'
? parseStateJson(stateJson)
: stateJson;
// Backend update dispatches a store action
store.dispatch(backendUpdate(state));
const message = typeof messageJson === 'string'
? parseStateJson(messageJson)
: messageJson;
logger.debug(`received message '${message?.type}'`);
const { type, payload } = message;
if (type === 'update') {
if (suspended) {
logger.log('resuming');
initialRender = 'recycled';
}
// Backend update dispatches a store action
store.dispatch(backendUpdate(payload));
return;
}
if (type === 'suspend') {
store.dispatch(backendSuspendSuccess());
return;
}
if (type === 'ping') {
sendMessage({
type: 'pingReply',
});
return;
}
// Pass the message directly to the store
store.dispatch(message);
};
// Enable hot module reloading
@@ -166,9 +183,26 @@ const setupApp = () => {
}
window.update(stateJson);
}
};
// Dynamically load font-awesome from browser's cache
loadCSS('font-awesome.css');
// Setup a fatal error reporter
window.__logger__ = {
fatal: (error, stack) => {
// Get last state for debugging purposes
const backendState = selectBackend(store.getState());
const reportedState = {
config: backendState.config,
suspended: backendState.suspended,
suspending: backendState.suspending,
};
// Send to development server
logger.log('FatalError:', error || stack);
logger.log('State:', reportedState);
// Append this data to the stack
stack += '\nState: ' + JSON.stringify(reportedState);
// Return an updated stack
return stack;
},
};
if (document.readyState === 'loading') {
+6 -3
View File
@@ -5,7 +5,7 @@
*/
import { classes } from 'common/react';
import { IS_IE8 } from '../byond';
import { computeBoxProps, computeBoxClassName } from '../components/Box';
/**
* Brings Layout__content DOM element back to focus.
@@ -14,7 +14,7 @@ import { IS_IE8 } from '../byond';
*/
export const refocusLayout = () => {
// IE8: Focus method is seemingly fucked.
if (IS_IE8) {
if (Byond.IS_LTE_IE8) {
return;
}
const element = document.getElementById('Layout__content');
@@ -47,6 +47,7 @@ const LayoutContent = props => {
className,
scrollable,
children,
...rest
} = props;
return (
<div
@@ -55,7 +56,9 @@ const LayoutContent = props => {
'Layout__content',
scrollable && 'Layout__content--scrollable',
className,
])}>
...computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{children}
</div>
);
+14 -5
View File
@@ -4,6 +4,7 @@
* @license MIT
*/
import { resolveAsset } from '../assets';
import { useBackend } from '../backend';
import { Box, Button } from '../components';
import { refocusLayout } from './Layout';
@@ -11,12 +12,16 @@ import { Window } from './Window';
export const NtosWindow = (props, context) => {
const {
title,
width = 575,
height = 700,
resizable,
theme = 'ntos',
children,
} = props;
const { act, data } = useBackend(context);
const {
PC_device_theme,
PC_batteryicon,
PC_showbatteryicon,
PC_batterypercent,
@@ -28,6 +33,9 @@ export const NtosWindow = (props, context) => {
} = data;
return (
<Window
title={title}
width={width}
height={height}
theme={theme}
resizable={resizable}>
<div className="NtosWindow">
@@ -41,7 +49,8 @@ export const NtosWindow = (props, context) => {
{PC_stationtime}
</Box>
<Box inline italic mr={2} opacity={0.33}>
NtOS
{PC_device_theme === 'ntos' && 'NtOS'}
{PC_device_theme === 'syndicate' && 'Syndix'}
</Box>
</div>
<div className="NtosHeader__right">
@@ -49,14 +58,14 @@ export const NtosWindow = (props, context) => {
<Box key={header.icon} inline mr={1}>
<img
className="NtosHeader__icon"
src={header.icon} />
src={resolveAsset(header.icon)} />
</Box>
))}
<Box inline>
{PC_ntneticon && (
<img
className="NtosHeader__icon"
src={PC_ntneticon} />
src={resolveAsset(PC_ntneticon)} />
)}
</Box>
{!!PC_showbatteryicon && PC_batteryicon && (
@@ -64,7 +73,7 @@ export const NtosWindow = (props, context) => {
{PC_batteryicon && (
<img
className="NtosHeader__icon"
src={PC_batteryicon} />
src={resolveAsset(PC_batteryicon)} />
)}
{PC_batterypercent && (
PC_batterypercent
@@ -75,7 +84,7 @@ export const NtosWindow = (props, context) => {
<Box inline mr={1}>
<img
className="NtosHeader__icon"
src={PC_apclinkicon} />
src={resolveAsset(PC_apclinkicon)} />
</Box>
)}
{!!PC_showexitprogram && (
+62 -24
View File
@@ -7,19 +7,35 @@
import { classes } from 'common/react';
import { decodeHtmlEntities, toTitleCase } from 'common/string';
import { Component, Fragment } from 'inferno';
import { useBackend } from '../backend';
import { IS_IE8, runCommand, winset } from '../byond';
import { Box, Icon } from '../components';
import { backendSuspendStart, useBackend } from '../backend';
import { Icon } from '../components';
import { UI_DISABLED, UI_INTERACTIVE, UI_UPDATE } from '../constants';
import { dragStartHandler, resizeStartHandler } from '../drag';
import { releaseHeldKeys } from '../hotkeys';
import { toggleKitchenSink, useDebug } from '../debug';
import { dragStartHandler, recallWindowGeometry, resizeStartHandler, setWindowKey } from '../drag';
import { createLogger } from '../logging';
import { useDispatch } from '../store';
import { Layout, refocusLayout } from './Layout';
const logger = createLogger('Window');
const DEFAULT_SIZE = [400, 600];
export class Window extends Component {
componentDidMount() {
const { config, suspended } = useBackend(this.context);
if (suspended) {
return;
}
logger.log('mounting');
const options = {
size: DEFAULT_SIZE,
...config.window,
};
if (this.props.width && this.props.height) {
options.size = [this.props.width, this.props.height];
}
setWindowKey(config.window.key);
recallWindowGeometry(config.window.key, options);
refocusLayout();
}
@@ -27,14 +43,18 @@ export class Window extends Component {
const {
resizable,
theme,
title,
children,
} = this.props;
const {
config,
debugLayout,
suspended,
} = useBackend(this.context);
const { debugLayout } = useDebug(this.context);
const dispatch = useDispatch(this.context);
const fancy = config.window?.fancy;
// Determine when to show dimmer
const showDimmer = config.observer
const showDimmer = config.user.observer
? config.status < UI_DISABLED
: config.status < UI_INTERACTIVE;
return (
@@ -43,27 +63,25 @@ export class Window extends Component {
theme={theme}>
<TitleBar
className="Window__titleBar"
title={decodeHtmlEntities(config.title)}
title={!suspended && (title || decodeHtmlEntities(config.title))}
status={config.status}
fancy={config.fancy}
fancy={fancy}
onDragStart={dragStartHandler}
onClose={() => {
logger.log('pressed close');
releaseHeldKeys();
winset(config.window, 'is-visible', false);
runCommand(`uiclose ${config.ref}`);
dispatch(backendSuspendStart());
}} />
<div
className={classes([
'Window__rest',
debugLayout && 'debug-layout',
])}>
{children}
{!suspended && children}
{showDimmer && (
<div className="Window__dimmer" />
)}
</div>
{config.fancy && resizable && (
{fancy && resizable && (
<Fragment>
<div className="Window__resizeHandle__e"
onMousedown={resizeStartHandler(1, 0)} />
@@ -79,15 +97,26 @@ export class Window extends Component {
}
const WindowContent = props => {
const { scrollable, children } = props;
const {
className,
fitted,
children,
...rest
} = props;
// A bit lazy to actually write styles for it,
// so we simply include a Box with margins.
return (
<Layout.Content
scrollable={scrollable}>
<Box m={1}>
{children}
</Box>
className={classes([
'Window__content',
className,
])}
{...rest}>
{fitted && children || (
<div className="Window__contentPadding">
{children}
</div>
)}
</Layout.Content>
);
};
@@ -106,7 +135,7 @@ const statusToColor = status => {
}
};
const TitleBar = props => {
const TitleBar = (props, context) => {
const {
className,
title,
@@ -115,6 +144,7 @@ const TitleBar = props => {
onDragStart,
onClose,
} = props;
const dispatch = useDispatch(context);
return (
<div
className={classes([
@@ -126,13 +156,21 @@ const TitleBar = props => {
color={statusToColor(status)}
name="eye" />
<div className="TitleBar__title">
{title === title.toLowerCase()
? toTitleCase(title)
: title}
{typeof title === 'string'
&& title === title.toLowerCase()
&& toTitleCase(title)
|| title}
</div>
<div
className="TitleBar__dragZone"
onMousedown={e => fancy && onDragStart(e)} />
{process.env.NODE_ENV !== 'production' && (
<div
className="TitleBar__devBuildIndicator"
onClick={() => dispatch(toggleKitchenSink())}>
<Icon name="bug" />
</div>
)}
{!!fancy && (
<div
className="TitleBar__close TitleBar__clickable"
@@ -140,7 +178,7 @@ const TitleBar = props => {
// IE8: Use a plain character instead of a unicode symbol.
// eslint-disable-next-line react/no-unknown-property
onclick={onClose}>
{IS_IE8 ? 'x' : '×'}
{Byond.IS_LTE_IE8 ? 'x' : '×'}
</div>
)}
</div>
+5 -5
View File
@@ -5,7 +5,6 @@
*/
import { sendLogEntry } from 'tgui-dev-server/link/client';
import { callByond } from './byond';
const LEVEL_DEBUG = 0;
const LEVEL_LOG = 1;
@@ -33,10 +32,11 @@ const log = (level, ns, ...args) => {
.filter(value => value)
.join(' ')
+ '\nUser Agent: ' + navigator.userAgent;
callByond('', {
src: window.__ref__,
action: 'tgui:log',
log: logEntry,
Byond.topic({
tgui: 1,
window_id: window.__windowId__,
type: 'log',
message: logEntry,
});
}
};
+5 -3
View File
@@ -1,7 +1,7 @@
{
"private": true,
"name": "tgui",
"version": "3.0.0",
"version": "3.1.0",
"dependencies": {
"@babel/core": "^7.6.2",
"@babel/plugin-transform-jscript": "^7.2.0",
@@ -12,16 +12,18 @@
"core-js": "^3.2.1",
"css-loader": "^3.2.0",
"cssnano": "^4.1.10",
"dompurify": "^2.0.11",
"extract-css-chunks-webpack-plugin": "^4.6.0",
"fg-loadcss": "^2.1.0",
"file-loader": "^6.0.0",
"inferno": "^7.3.2",
"marked": "^1.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"regenerator-runtime": "^0.13.3",
"sass": "^1.22.12",
"sass-loader": "^8.0.0",
"sass-loader": "^9.0.2",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^2.1.0",
"terser-webpack-plugin": "^3.0.6",
"url-loader": "^4.1.0",
"webpack": "^4.40.2",
"webpack-build-notifier": "^2.0.0",
+125 -27
View File
@@ -5,29 +5,59 @@
<meta charset="utf-8">
<!-- Inlined data -->
<meta id="tgui:ref" content="[tgui:ref]">
<meta id="tgui:windowId" content="[tgui:windowId]">
<!-- Early setup -->
<script type="text/javascript">
// Mark the beginning of initialization
window.__inception__ = new Date().getTime();
// Read tgui object ref into a global
window.__ref__ = document
.getElementById('tgui:ref')
// Read window id into a global
window.__windowId__ = document
.getElementById('tgui:windowId')
.getAttribute('content');
// Null if template marker was not replaced
if (window.__ref__ === '[' + 'tgui:ref' + ']') {
window.__ref__ = null;
if (window.__windowId__ === '[' + 'tgui:windowId' + ']') {
window.__windowId__ = null;
}
// BYOND API object
window.Byond = (function () {
var Byond = {};
// Utility functions
var hasOwn = Object.prototype.hasOwnProperty;
var assign = function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (hasOwn.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
// Trident engine version
var tridentVersion = (function () {
var groups = navigator.userAgent.match(/Trident\/(\d+).+?;/i);
var majorVersion = groups && groups[1];
return majorVersion
? parseInt(majorVersion, 10)
: null;
})();
// Basic checks to detect whether this page runs in BYOND
var isByond = !!navigator.userAgent.match(/Trident\/(\d+).+?;/i)
var isByond = tridentVersion !== null
&& location.hostname === '127.0.0.1'
&& location.pathname.indexOf('/tmp') === 0;
// Version constants
Byond.IS_BYOND = isByond;
Byond.IS_LTE_IE8 = tridentVersion !== null && tridentVersion <= 4;
Byond.IS_LTE_IE9 = tridentVersion !== null && tridentVersion <= 5;
Byond.IS_LTE_IE10 = tridentVersion !== null && tridentVersion <= 6;
Byond.IS_LTE_IE11 = tridentVersion !== null && tridentVersion <= 7;
// Callbacks for asynchronous calls
Byond.__callbacks__ = [];
// Makes a BYOND call.
// See: https://secure.byond.com/docs/ref/skinparams.html
Byond.call = function (path, params) {
@@ -64,8 +94,61 @@ window.Byond = (function () {
xhr.open('GET', url);
xhr.send();
};
Byond.callAsync = function (path, params) {
if (!window.Promise) {
throw new Error('Async calls require API level of ES2015 or later.');
}
var index = Byond.__callbacks__.length;
var promise = new window.Promise(function (resolve) {
Byond.__callbacks__.push(resolve);
});
Byond.call(path, assign({}, params, {
callback: 'Byond.__callbacks__[' + index + ']',
}));
return promise;
};
Byond.topic = function (params) {
return Byond.call('', params);
};
Byond.command = function (command) {
return Byond.call('winset', {
command: command,
});
};
Byond.winget = function (id, propName) {
var isArray = propName instanceof Array;
var isSpecific = propName && propName !== '*' && !isArray;
var promise = Byond.callAsync('winget', {
id: id,
property: isArray && propName.join(',') || propName || '*',
});
if (isSpecific) {
promise = promise.then(function (props) {
return props[propName];
});
}
return promise;
};
Byond.winset = function (id, propName, propValue) {
var props = {};
if (propName === 'string') {
props[propName] = propValue;
}
else {
assign(props, propName);
}
props.id = id;
return Byond.call('winset', props);
};
return Byond;
})();
// Global error handling
window.onerror = function (msg, url, line, col, error) {
// Proper stacktrace
@@ -78,7 +161,7 @@ window.onerror = function (msg, url, line, col, error) {
}
}
// Append user agent info
stack += '\n\nUser Agent: ' + navigator.userAgent;
stack += '\nUser Agent: ' + navigator.userAgent;
// Print error to the page
var errorRoot = document.getElementById('FatalError');
var errorStack = document.getElementById('FatalError__stack');
@@ -91,12 +174,29 @@ window.onerror = function (msg, url, line, col, error) {
errorStack.textContent = stack;
}
}
// Send logs to the server
Byond.call('', {
src: window.__ref__,
action: 'tgui:log',
fatal: '1',
log: stack,
// Set window geometry
var setFatalErrorGeometry = function () {
Byond.winset(window.__windowId__, {
titlebar: true,
size: '600x600',
'is-visible': true,
'can-resize': true,
});
};
setFatalErrorGeometry();
setInterval(setFatalErrorGeometry, 1000);
// Use a hook that comes with the bundle to augment the stack
// and send errors to a dev server (if using a development build).
if (window.__logger__) {
stack = window.__logger__.fatal(error, stack);
}
// Send logs to the game server
Byond.topic({
tgui: 1,
window_id: window.__windowId__,
type: 'log',
fatal: 1,
message: stack,
});
// Short-circuit further updates
window.__updateQueue__ = [];
@@ -104,26 +204,24 @@ window.onerror = function (msg, url, line, col, error) {
// Prevent default action
return true;
};
// Early initialization
window.__updateQueue__ = [];
window.update = function (stateJson) {
window.__updateQueue__.push(stateJson);
window.update = function (message) {
window.__updateQueue__.push(message);
};
Byond.call('', {
src: window.__ref__,
action: 'tgui:initialize',
Byond.topic({
tgui: 1,
window_id: window.__windowId__,
type: 'ready',
});
</script>
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="tgui.bundle.css">
<!-- This is processed in byond, so interfaces can override the
html head if needed, for custom sheets of style etc. -->
<!--customheadhtml-->
<!-- tgui:styles -->
<!-- Scripts -->
<script type="text/javascript" defer src="tgui.bundle.js"></script>
<!-- tgui:scripts -->
</head>
<body>
@@ -161,7 +259,7 @@ A fatal exception has occurred at 002B:C562F1B7 in TGUI.
The current application will be terminated.
Please remain calm. Get to the nearest NTNet workstation
and send the copy of the following stack trace to:
github.com/Citadel-Station-13/Citadel-Station-13/issues. Thank you for your cooperation.
https://github.com/Citadel-Station-13/Citadel-Station-13/issues Thank you for your cooperation.
</marquee>
<div id="FatalError__stack" class="FatalError__stack"></div>
<div class="FatalError__footer">
+18 -3
View File
@@ -4,6 +4,8 @@
* @license MIT
*/
import { selectBackend } from './backend';
import { selectDebug } from './debug';
import { Window } from './layouts';
const requireInterface = require.context('./interfaces', false, /\.js$/);
@@ -23,14 +25,27 @@ const routingError = (type, name) => () => {
);
};
const SuspendedWindow = () => {
return (
<Window resizable>
<Window.Content scrollable />
</Window>
);
};
export const getRoutedComponent = state => {
const { suspended, config } = selectBackend(state);
if (suspended) {
return SuspendedWindow;
}
if (process.env.NODE_ENV !== 'production') {
const debug = selectDebug(state);
// Show a kitchen sink
if (state.showKitchenSink) {
return require('./interfaces/manually-routed/KitchenSink').KitchenSink;
if (debug.kitchenSink) {
return require('./debug/KitchenSink').KitchenSink;
}
}
const name = state.config?.interface;
const name = config?.interface;
let esModule;
try {
esModule = requireInterface(`./${name}.js`);
+28 -8
View File
@@ -5,24 +5,44 @@
*/
import { flow } from 'common/fp';
import { applyMiddleware, createStore as createReduxStore } from 'common/redux';
import { applyMiddleware, combineReducers, createStore as createReduxStore } from 'common/redux';
import { Component } from 'inferno';
import { backendReducer } from './backend';
import { hotKeyMiddleware, hotKeyReducer } from './hotkeys';
import { backendMiddleware, backendReducer } from './backend';
import { debugReducer } from './debug';
import { hotKeyMiddleware } from './hotkeys';
import { createLogger } from './logging';
import { assetMiddleware } from './assets';
const logger = createLogger('store');
export const createStore = () => {
const reducer = flow([
// State initializer
(state = {}, action) => state,
// Global state reducers
backendReducer,
hotKeyReducer,
combineReducers({
debug: debugReducer,
backend: backendReducer,
}),
]);
const middleware = [
// loggingMiddleware,
process.env.NODE_ENV !== 'production' && loggingMiddleware,
assetMiddleware,
hotKeyMiddleware,
backendMiddleware,
];
return createReduxStore(reducer, applyMiddleware(...middleware));
return createReduxStore(reducer,
applyMiddleware(...middleware.filter(Boolean)));
};
const loggingMiddleware = store => next => action => {
const { type, payload } = action;
if (type === 'backend/update') {
logger.debug('action', { type });
}
else {
logger.debug('action', action);
}
return next(action);
};
export class StoreProvider extends Component {
@@ -7,42 +7,34 @@
.outline-dotted {
outline-style: dotted !important;
outline-width: 2px !important;
}
.outline-dashed {
outline-style: dashed !important;
outline-width: 2px !important;
}
.outline-solid {
outline-style: solid !important;
outline-width: 2px !important;
}
.outline-double {
outline-style: double !important;
outline-width: 2px !important;
}
.outline-groove {
outline-style: groove !important;
outline-width: 2px !important;
}
.outline-ridge {
outline-style: ridge !important;
outline-width: 2px !important;
}
.outline-inset {
outline-style: inset !important;
outline-width: 2px !important;
}
.outline-outset {
outline-style: outset !important;
outline-width: 2px !important;
}
$fg-map: colors.$fg-map !default;
@@ -50,6 +42,6 @@ $bg-map: colors.$bg-map !default;
@each $color-name, $color-value in $fg-map {
.outline-color-#{$color-name} {
outline-color: $color-value !important;
outline: 0.167rem solid $color-value !important;
}
}
+12 -1
View File
@@ -12,4 +12,15 @@ $color-bg-start: color.adjust(
$color-bg, $lightness: $color-bg-grad-spread) !default;
$color-bg-end: color.adjust(
$color-bg, $lightness: -$color-bg-grad-spread) !default;
$border-radius: 2px !default;
$unit: 12px;
$font-size: 1 * $unit !default;
$border-radius: 0.16em !default;
@function em($px) {
@return 1em * ($px / $unit);
}
@function rem($px) {
@return 1rem * ($px / $unit);
}
@@ -10,9 +10,9 @@ $color-default: colors.fg(colors.$label) !default;
.BlockQuote {
color: $color-default;
border-left: 2px solid $color-default;
padding-left: 6px;
margin-bottom: 6px;
border-left: base.em(2px) solid $color-default;
padding-left: 0.5em;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
@@ -45,13 +45,13 @@ $bg-map: colors.$bg-map !default;
.Button {
position: relative;
display: inline-block;
line-height: 20px;
padding: 0 6px;
margin-right: 2px;
line-height: base.em(20px);
padding: 0 0.5em;
margin-right: base.em(2px);
white-space: nowrap;
outline: 0;
border-radius: $border-radius;
margin-bottom: 2px;
margin-bottom: base.em(2px);
// Disable selection in buttons
user-select: none;
-ms-user-select: none;
@@ -61,9 +61,9 @@ $bg-map: colors.$bg-map !default;
}
.fa, .fas, .far {
margin-left: -3px;
margin-right: -3px;
min-width: 16px;
margin-left: -0.25em;
margin-right: -0.25em;
min-width: base.em(16px);
text-align: center;
}
}
@@ -71,7 +71,7 @@ $bg-map: colors.$bg-map !default;
.Button--hasContent {
// Add a margin to the icon to keep it separate from the text
.fa, .fas, .far {
margin-right: 3px;
margin-right: 0.25em;
}
}
@@ -5,8 +5,8 @@
.ColorBox {
display: inline-block;
width: 12px;
height: 12px;
line-height: 12px;
width: 1em;
height: 1em;
line-height: 1em;
text-align: center;
}
@@ -3,9 +3,11 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
$color: rgba(255, 255, 255, 0.1) !default;
$thickness: 2px !default;
$spacing: 6px;
$thickness: base.em(2px) !default;
$spacing: 0.5em;
.Divider--horizontal {
margin: $spacing 0;
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
.Dropdown {
position: relative;
}
@@ -11,27 +13,27 @@
position: relative;
display: inline-block;
font-family: Verdana, sans-serif;
font-size: 12px;
width: 100px;
line-height: 17px;
font-size: base.em(12px);
width: base.em(100px);
line-height: base.em(17px);
user-select: none;
}
.Dropdown__arrow-button {
float: right;
padding-left: 6px;
border-left: 1px solid #000;
border-left: 1px solid rgba(0, 0, 0, 0.25);
padding-left: 0.5em;
border-left: base.em(1px) solid #000;
border-left: base.em(1px) solid rgba(0, 0, 0, 0.25);
}
.Dropdown__menu {
position: absolute;
overflow-y: auto;
z-index: 5;
width: 100px;
max-height: 200px;
width: base.em(100px);
max-height: base.em(200px);
overflow-y: scroll;
border-radius: 0 0 2px 2px;
border-radius: 0 0 base.em(2px) base.em(2px);
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
}
@@ -40,18 +42,18 @@
position: absolute;
overflow-y: auto;
z-index: 5;
width: 100px;
max-height: 200px;
border-radius: 0 0 2px 2px;
width: base.em(100px);
max-height: base.em(200px);
border-radius: 0 0 base.em(2px) base.em(2px);
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
}
.Dropdown__menuentry {
padding: 2px 4px;
padding: base.em(2px) base.em(4px);
font-family: Verdana, sans-serif;
font-size: 12px;
line-height: 17px;
font-size: base.em(12px);
line-height: base.em(17px);
transition: background-color 100ms;
&:hover {
background-color: #444;
@@ -12,30 +12,30 @@
display: inline-flex;
}
.Flex--ie8 {
.Flex--iefix {
display: table !important;
}
.Flex--ie8--column {
.Flex--iefix--column {
display: block !important;
& > .Flex__item {
display: block !important;
margin-left: 6px;
margin-right: 6px;
margin-left: 0.5em;
margin-right: 0.5em;
}
}
.Flex__item--ie8 {
.Flex__item--iefix {
display: table-cell !important;
}
@for $i from 1 through 2 {
.Flex--spacing--#{$i} {
margin: 0 -3px * $i;
margin: 0 -0.25em * $i;
& > .Flex__item {
margin: 0 3px * $i;
margin: 0 0.25em * $i;
}
}
}
+10 -10
View File
@@ -12,16 +12,16 @@ $border-radius: base.$border-radius !default;
.Input {
position: relative;
display: inline-block;
width: 120px;
border: 1px solid $border-color;
border: 1px solid rgba($border-color, 0.75);
width: base.em(120px);
border: base.em(1px) solid $border-color;
border: base.em(1px) solid rgba($border-color, 0.75);
border-radius: $border-radius;
color: #fff;
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
padding: 0 base.em(4px);
margin-right: base.em(2px);
line-height: base.em(17px);
overflow: visible;
}
@@ -45,11 +45,11 @@ $border-radius: base.$border-radius !default;
border: 0;
outline: 0;
width: 100%;
font-size: 12px;
line-height: 17px;
height: 17px;
font-size: base.em(12px);
line-height: base.em(17px);
height: base.em(17px);
margin: 0;
padding: 0 6px;
padding: 0 0.5em;
font-family: Verdana, sans-serif;
background-color: transparent;
color: #fff;
@@ -124,41 +124,3 @@ $pi: 3.1416;
}
}
}
// .Slider__cursorOffset {
// position: absolute;
// top: 0;
// left: 0;
// bottom: 0;
// transition: none !important;
// }
// .Slider__cursor {
// position: absolute;
// top: 0;
// right: -1px;
// bottom: 0;
// width: 0;
// border-left: 2px solid #fff;
// }
// .Slider__pointer {
// position: absolute;
// right: -5px;
// bottom: -4px;
// width: 0;
// height: 0;
// border-left: 5px solid transparent;
// border-right: 5px solid transparent;
// border-bottom: 5px solid #fff;
// }
// .Slider__popupValue {
// position: absolute;
// right: 0;
// top: -22px;
// padding: 2px 4px;
// background-color: #000;
// transform: translateX(50%);
// white-space: nowrap;
// }
@@ -3,15 +3,17 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
.LabeledList {
display: table;
// IE8: Does not support calc
width: 100%;
// Compensate for negative margin
width: calc(100% + 12px);
width: calc(100% + 1em);
border-collapse: collapse;
border-spacing: 0;
margin: -3px -6px;
margin: -0.25em -0.5em;
margin-bottom: 0;
padding: 0;
}
@@ -27,7 +29,7 @@
.LabeledList__cell {
display: table-cell;
margin: 0;
padding: 3px 6px;
padding: 0.25em 0.5em;
border: 0;
text-align: left;
vertical-align: baseline;
@@ -36,13 +38,13 @@
.LabeledList__label {
width: 1%;
white-space: nowrap;
min-width: 60px;
min-width: 5em;
}
.LabeledList__buttons {
width: 0.1%;
white-space: nowrap;
text-align: right;
padding-top: 1px;
padding-top: base.em(1px);
padding-bottom: 0;
}
@@ -4,6 +4,7 @@
*/
@use 'sass:color';
@use '../base.scss';
@use '../colors.scss';
@use '../functions.scss' as *;
@@ -20,8 +21,8 @@ $bg-map: colors.$bg-map !default;
rgba(0, 0, 0, 1),
rgba(255, 255, 255, 1));
padding: 4px 6px;
margin-bottom: 6px;
padding: 0.33em 0.5em;
margin-bottom: 0.5em;
box-shadow: none;
font-weight: bold;
font-style: italic;
@@ -30,9 +31,9 @@ $bg-map: colors.$bg-map !default;
background-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 10px,
$color-stripes 10px,
$color-stripes 20px);
transparent base.em(10px),
$color-stripes base.em(10px),
$color-stripes base.em(20px));
}
@mixin box-color($color) {
@@ -4,6 +4,7 @@
*/
@use 'sass:color';
@use '../base.scss';
@use '../functions.scss' as *;
@use './Input.scss';
@@ -13,15 +14,15 @@ $border-radius: Input.$border-radius !default;
.NumberInput {
position: relative;
display: inline-block;
border: 1px solid $border-color;
border: 1px solid rgba($border-color, 0.75);
border: base.em(1px) solid $border-color;
border: base.em(1px) solid rgba($border-color, 0.75);
border-radius: $border-radius;
color: $border-color;
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
padding: 0 base.em(4px);
margin-right: base.em(2px);
line-height: base.em(17px);
text-align: right;
overflow: visible;
cursor: n-resize;
@@ -32,23 +33,23 @@ $border-radius: Input.$border-radius !default;
}
.NumberInput__content {
margin-left: 6px;
margin-left: 0.5em;
}
.NumberInput__barContainer {
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
top: base.em(2px);
bottom: base.em(2px);
left: base.em(2px);
}
.NumberInput__bar {
position: absolute;
bottom: 0;
left: 0;
width: 3px;
width: base.em(3px);
box-sizing: border-box;
border-bottom: 1px solid $border-color;
border-bottom: base.em(1px) solid $border-color;
background-color: $border-color;
}
@@ -62,11 +63,11 @@ $border-radius: Input.$border-radius !default;
border: 0;
outline: 0;
width: 100%;
font-size: 12px;
line-height: 17px;
height: 17px;
font-size: base.em(12px);
line-height: base.em(17px);
height: base.em(17px);
margin: 0;
padding: 0 6px;
padding: 0 0.5em;
font-family: Verdana, sans-serif;
background-color: #000;
color: #fff;
@@ -16,7 +16,7 @@ $bg-map: colors.$bg-map !default;
display: inline-block;
position: relative;
width: 100%;
padding: 0 6px;
padding: 0 0.5em;
border-radius: $border-radius;
background-color: $color-background;
transition: border-color 500ms;
@@ -24,9 +24,9 @@ $bg-map: colors.$bg-map !default;
.ProgressBar__fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
top: -0.5px;
left: 0px;
bottom: -0.5px;
}
.ProgressBar__fill--animated {
@@ -35,13 +35,13 @@ $bg-map: colors.$bg-map !default;
.ProgressBar__content {
position: relative;
line-height: 17px;
line-height: base.em(17px);
width: 100%;
text-align: right;
}
.ProgressBar--color--default {
border: 1px solid $color-default;
border: base.em(1px) solid $color-default;
.ProgressBar__fill {
background-color: $color-default;
@@ -50,7 +50,7 @@ $bg-map: colors.$bg-map !default;
@each $color-name, $color-value in $bg-map {
.ProgressBar--color--#{$color-name} {
border: 1px solid $color-value !important;
border: base.em(1px) solid $color-value !important;
.ProgressBar__fill {
background-color: $color-value;
@@ -4,6 +4,7 @@
*/
@use 'sass:color';
@use '../base.scss';
@use '../colors.scss';
$color-title: #ffffff !default;
@@ -13,7 +14,7 @@ $color-separator: colors.$primary !default;
.Section {
position: relative;
margin-bottom: 6px;
margin-bottom: 0.5em;
// IE8: Does not support rgba colors
background-color: color.adjust(
$color-background,
@@ -30,42 +31,52 @@ $color-separator: colors.$primary !default;
.Section__title {
position: relative;
padding: 6px;
border-bottom: 2px solid $color-separator;
padding: 0.5em;
border-bottom: base.em(2px) solid $color-separator;
}
.Section__titleText {
font-size: 14px;
font-size: base.em(14px);
font-weight: bold;
}
.Section__buttons {
position: absolute;
display: inline-block;
right: 6px;
margin-top: -1px;
right: 0.5em;
margin-top: base.em(-1px);
}
.Section__content {
padding: 8px 6px;
padding: 0.66em 0.5em;
}
.Section--fill {
display: flex;
flex-direction: column;
height: 100%;
}
.Section--fill .Section__content {
flex-grow: 1;
}
.Section--level--1 .Section__titleText {
font-size: 14px;
font-size: base.em(14px);
}
.Section--level--2 .Section__titleText {
font-size: 13px;
font-size: base.em(13px);
}
.Section--level--3 .Section__titleText {
font-size: 12px;
font-size: base.em(12px);
}
.Section--level--2,
.Section--level--3 {
background-color: transparent;
box-shadow: none;
margin-left: -6px;
margin-right: -6px;
margin-left: -0.5em;
margin-right: -0.5em;
}
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
.Slider {
cursor: e-resize;
}
@@ -18,28 +20,29 @@
.Slider__cursor {
position: absolute;
top: 0;
right: -1px;
right: base.em(-1px);
bottom: 0;
width: 0;
border-left: 2px solid #fff;
border-left: base.em(2px) solid #fff;
}
.Slider__pointer {
position: absolute;
right: -5px;
bottom: -4px;
right: base.em(-5px);
bottom: base.em(-4px);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #fff;
border-left: base.em(5px) solid transparent;
border-right: base.em(5px) solid transparent;
border-bottom: base.em(5px) solid #fff;
}
.Slider__popupValue {
position: absolute;
right: 0;
top: -22px;
padding: 2px 4px;
top: -2rem;
font-size: 1rem;
padding: 0.25rem 0.5rem;
background-color: #000;
transform: translateX(50%);
white-space: nowrap;
@@ -21,7 +21,7 @@
.Table__cell {
display: table-cell;
padding: 0 3px;
padding: 0 0.25em;
&:first-child {
padding-left: 0;
@@ -35,7 +35,7 @@
.Table__row--header .Table__cell,
.Table__cell--header {
font-weight: bold;
padding-bottom: 6px;
padding-bottom: 0.5em;
}
.Table__cell--collapsing {
@@ -8,8 +8,8 @@
$border-radius: base.$border-radius !default;
.Tabs--horizontal {
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
margin-bottom: 6px;
border-bottom: base.em(2px) solid rgba(255, 255, 255, 0.1);
margin-bottom: 0.5em;
.Tabs__tab--altSelection::after {
content: '';
@@ -17,7 +17,7 @@ $border-radius: base.$border-radius !default;
bottom: 0;
right: 0;
left: 0;
height: 2px;
height: base.em(2px);
width: 100%;
background-color: #fff;
border-radius: $border-radius;
@@ -25,11 +25,10 @@ $border-radius: base.$border-radius !default;
}
.Tabs--vertical {
margin-right: 9px;
margin-right: 0.75em;
.Tabs__tabBox {
// padding-right: 2px;
border-right: 2px solid rgba(255, 255, 255, 0.1);
border-right: base.em(2px) solid rgba(255, 255, 255, 0.1);
// Disable baseline alignment when doing vertical tabs
vertical-align: top;
}
@@ -40,8 +39,8 @@ $border-radius: base.$border-radius !default;
// Override to stop themed buttons from taking priority over it.
margin-right: 0 !important;
margin-bottom: 0;
padding: 1px 9px 0px 6px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
padding: base.em(1px) base.em(9px) 0px 0.5em;
border-bottom: base.em(2px) solid rgba(255, 255, 255, 0.1);
&:last-child {
border-bottom: 0;
@@ -55,7 +54,7 @@ $border-radius: base.$border-radius !default;
bottom: 0;
right: 0;
height: 100%;
width: 3px;
width: 0.25em;
background-color: #fff;
border-radius: $border-radius;
}
@@ -3,96 +3,96 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
@use '../functions.scss' as *;
@use '../base.scss';
@use '../functions.scss' as *;
$text-color: #272727 !default;
$border-color: #88bfff !default;
$border-radius: base.$border-radius !default;
$color-background: #bb9b68 !default;
$color-border: #272727 !default;
$text-color: #272727 !default;
$border-color: #88bfff !default;
$border-radius: base.$border-radius !default;
$color-background: #bb9b68 !default;
$color-border: #272727 !default;
.TextArea {
overflow: hidden;
position: relative;
display: inline-block;
height: "100%";
border: 1px solid $border-color;
border: 1px solid rgba($border-color, 0.75);
border-radius: $border-radius;
background-color: $color-background;
margin-right: 2px;
line-height: 17px;
overflow: visible;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
width:100%;
.TextArea {
overflow: hidden;
position: relative;
display: inline-block;
height: "100%";
border: 1px solid $border-color;
border: 1px solid rgba($border-color, 0.75);
border-radius: $border-radius;
background-color: $color-background;
margin-right: 2px;
line-height: 17px;
overflow: visible;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
width:100%;
.TextArea_filler {
word-wrap: break-word; /* make sure the div and the textarea wrap words in the same way */
box-sizing: border-box;
padding: 2px;
width: 100%;
.TextArea_filler {
word-wrap: break-word; /* make sure the div and the textarea wrap words in the same way */
box-sizing: border-box;
padding: 2px;
width: 100%;
padding-bottom: 1.5em; /* A bit more than one additional line of text. */
visibility: hidden;
}
padding-bottom: 1.5em; /* A bit more than one additional line of text. */
visibility: hidden;
}
.TextArea--fluid {
display: block;
width: auto;
height: auto;
}
.TextArea--fluid {
display: block;
width: auto;
height: auto;
}
.TextArea__baseline {
display: inline-block;
color: transparent;
}
.TextArea__baseline {
display: inline-block;
color: transparent;
}
.TextArea__textarea {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 0;
outline: 0;
width: 100%;
height: 100%;
font-size: 12px;
line-height: 17px;
height: 17px;
margin: 0;
padding: 0 6px;
font-family: inherit;
background-color: transparent;
color: inherit;
box-sizing: border-box;
width: 100%;
.TextArea__textarea {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 0;
outline: 0;
width: 100%;
height: 100%;
font-size: 12px;
line-height: 17px;
height: 17px;
margin: 0;
padding: 0 6px;
font-family: inherit;
background-color: transparent;
color: inherit;
box-sizing: border-box;
width: 100%;
/* Cut and paste to auto resize the textarea */
word-wrap: break-word; /* make sure the div and the textarea wrap words in the same way */
box-sizing: border-box;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
/* Cut and paste to auto resize the textarea */
word-wrap: break-word; /* make sure the div and the textarea wrap words in the same way */
box-sizing: border-box;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
padding: 2px;
width: 100%;
padding: 2px;
width: 100%;
overflow: hidden;
position: absolute;
height: 100%;
overflow: hidden;
position: absolute;
height: 100%;
&:-ms-input-placeholder {
font-style: italic;
color: #777;
/* color: rgba(255, 255, 255, 0.45);*/
}
}
&:-ms-input-placeholder {
font-style: italic;
color: #777;
/* color: rgba(255, 255, 255, 0.45);*/
}
}
}
}
@@ -23,7 +23,7 @@ $border-radius: base.$border-radius !default;
display: block;
white-space: nowrap;
z-index: 2;
padding: 6px 10px;
padding: 0.5em 0.75em;
transform: translateX(-50%);
pointer-events: none;
visibility: hidden;
@@ -32,7 +32,7 @@ $border-radius: base.$border-radius !default;
content: attr(data-tooltip);
transition: all 150ms;
background-color: $color-background;
box-shadow: 1px 1px 15px -1px rgba(0, 0, 0, 0.5);
box-shadow: 0.1em 0.1em 1.25em -0.1em rgba(0, 0, 0, 0.5);
border-radius: $border-radius;
}
@@ -46,7 +46,7 @@ $border-radius: base.$border-radius !default;
.Tooltip--long {
&::after {
width: 250px;
width: base.em(250px);
white-space: normal;
}
}
@@ -54,59 +54,59 @@ $border-radius: base.$border-radius !default;
.Tooltip--top::after {
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(8px);
transform: translateX(-50%) translateY(0.5em);
}
.Tooltip--top:hover::after {
transform: translateX(-50%) translateY(-8px);
transform: translateX(-50%) translateY(-0.5em);
}
.Tooltip--bottom::after {
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(-8px);
transform: translateX(-50%) translateY(-0.5em);
}
.Tooltip--bottom:hover::after {
transform: translateX(-50%) translateY(8px);
transform: translateX(-50%) translateY(0.5em);
}
.Tooltip--bottom-left::after {
top: 100%;
right: 50%;
transform: translateX(12px) translateY(-8px);
transform: translateX(12px) translateY(-0.5em);
}
.Tooltip--bottom-left:hover::after {
transform: translateX(12px) translateY(8px);
transform: translateX(12px) translateY(0.5em);
}
.Tooltip--bottom-right::after {
top: 100%;
left: 50%;
transform: translateX(-12px) translateY(-8px);
transform: translateX(-12px) translateY(-0.5em);
}
.Tooltip--bottom-right:hover::after {
transform: translateX(-12px) translateY(8px);
transform: translateX(-12px) translateY(0.5em);
}
.Tooltip--left::after {
top: 50%;
right: 100%;
transform: translateX(8px) translateY(-50%);
transform: translateX(0.5em) translateY(-50%);
}
.Tooltip--left:hover::after {
transform: translateX(-8px) translateY(-50%);
transform: translateX(-0.5em) translateY(-50%);
}
.Tooltip--right::after {
top: 50%;
left: 100%;
transform: translateX(-8px) translateY(-50%);
transform: translateX(-0.5em) translateY(-50%);
}
.Tooltip--right:hover::after {
transform: translateX(8px) translateY(-50%);
transform: translateX(0.5em) translateY(-50%);
}
@@ -1,3 +1,5 @@
@use '../base.scss';
$color-background: rgba(0, 0, 0, 0.33) !default;
.CameraConsole__left {
@@ -5,14 +7,14 @@ $color-background: rgba(0, 0, 0, 0.33) !default;
top: 0;
bottom: 0;
left: 0;
width: 220px;
width: base.em(220px);
}
.CameraConsole__right {
position: absolute;
top: 0;
bottom: 0;
left: 220px;
left: base.em(220px);
right: 0;
background-color: $color-background;
}
@@ -22,33 +24,30 @@ $color-background: rgba(0, 0, 0, 0.33) !default;
top: 0;
left: 0;
right: 0;
height: 24px;
line-height: 24px;
margin: 3px 12px 0;
// background-color: #0a0;
height: 2em;
line-height: 2em;
margin: 0.25em 1em 0;
}
.CameraConsole__toolbarRight {
position: absolute;
top: 0;
right: 0;
height: 24px;
line-height: 24px;
margin: 4px 6px 0;
// background-color: #aa0;
height: 2em;
line-height: 2em;
margin: 0.33em 0.5em 0;
}
.CameraConsole__map {
position: absolute;
top: 26px;
top: base.em(26px);
bottom: 0;
left: 0;
right: 0;
// background-color: #00a;
margin: 6px;
margin: 0.5em;
text-align: center;
.NoticeBox {
margin-top: calc(50% - 24px);
margin-top: calc(50% - 2em);
}
}
@@ -9,11 +9,18 @@ $background-beige: #E8E4C9;
.NuclearBomb__displayBox {
background-color: #002003;
border: 4px inset $background-beige;
border: 0.167em inset $background-beige;
color: #03e017;
font-size: 24px;
font-size: 2em;
font-family: monospace;
padding: 6px;
padding: 0.25em;
}
.NuclearBomb__Button {
outline-width: 0.25rem !important;
border-width: 0.65rem !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.NuclearBomb__Button--keypad {
@@ -1,3 +1,4 @@
@use '../base.scss';
@use '../colors.scss';
.Roulette {
+12 -10
View File
@@ -6,16 +6,8 @@
@use 'sass:color';
@use '../base.scss';
.Layout__content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-x: hidden;
overflow-y: hidden;
margin-bottom: -6px;
.Layout,
.Layout * {
// Fancy scrollbar
scrollbar-base-color: color.scale(
base.$color-bg,
@@ -40,6 +32,16 @@
$lightness: 10%);
}
.Layout__content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-x: hidden;
overflow-y: hidden;
}
.Layout__content--scrollable {
overflow-y: scroll;
margin-bottom: 0;
@@ -5,16 +5,16 @@
.NtosHeader__left {
position: absolute;
left: 12px;
left: 1em;
}
.NtosHeader__right {
position: absolute;
right: 12px;
right: 1em;
}
.NtosHeader__icon {
margin-top: -9px;
margin-bottom: -6px;
margin-top: -0.75em;
margin-bottom: -0.5em;
vertical-align: middle;
}
@@ -3,22 +3,24 @@
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
.NtosWindow__header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 28px;
line-height: 27px;
height: 2em;
line-height: 1.928em;
background-color: rgba(0, 0, 0, 0.5);
font-family: Consolas, monospace;
font-size: 14px;
font-size: base.em(14px);
user-select: none;
-ms-user-select: none;
}
.NtosWindow__content .Layout__content {
margin-top: 28px;
margin-top: 2em;
font-family: Consolas, monospace;
font-size: 14px;
font-size: base.em(14px);
}
@@ -4,6 +4,8 @@
*/
@use 'sass:color';
@use '../base.scss';
@use '../colors.scss';
$color-text: rgba(255, 255, 255, 0.75) !default;
$color-background: #363636 !default;
@@ -14,6 +16,7 @@ $color-shadow: rgba(0, 0, 0, 0.1) !default;
background-color: $color-background;
border-bottom: 1px solid $color-shadow-core;
box-shadow: 0 2px 2px $color-shadow;
box-shadow: 0 base.rem(2px) base.rem(2px) $color-shadow;
user-select: none;
-ms-user-select: none;
}
@@ -34,9 +37,12 @@ $color-shadow: rgba(0, 0, 0, 0.1) !default;
position: absolute;
top: 0;
left: 46px;
left: base.rem(46px);
color: $color-text;
font-size: 14px;
font-size: base.rem(14px);
line-height: 31px;
line-height: base.rem(31px);
white-space: nowrap;
}
@@ -46,21 +52,19 @@ $color-shadow: rgba(0, 0, 0, 0.1) !default;
left: 0;
right: 0;
height: 32px;
height: base.rem(32px);
}
.TitleBar__statusIcon {
position: absolute;
top: 0;
left: 12px;
left: base.rem(12px);
transition: color 0.5s;
font-size: 20px;
font-size: base.rem(20px);
line-height: 32px !important;
}
.TitleBar__minimize {
position: absolute;
top: 6px;
right: 46px;
line-height: base.rem(32px) !important;
}
.TitleBar__close {
@@ -68,8 +72,27 @@ $color-shadow: rgba(0, 0, 0, 0.1) !default;
top: -1px;
right: 0;
width: 45px;
width: base.rem(45px);
height: 32px;
height: base.rem(32px);
font-size: 20px;
font-size: base.rem(20px);
line-height: 31px;
line-height: base.rem(31px);
text-align: center;
}
.TitleBar__devBuildIndicator {
position: absolute;
top: 6px;
top: base.rem(6px);
right: 52px;
right: base.rem(52px);
min-width: 20px;
min-width: base.rem(20px);
padding: 2px 4px;
padding: base.rem(2px) base.rem(4px);
background-color: rgba(colors.$good, 0.75);
color: #fff;
text-align: center;
}
+23 -13
View File
@@ -27,17 +27,36 @@
left: 0;
width: 100%;
height: 32px;
height: base.rem(32px);
}
// Everything after the title bar
.Window__rest {
position: fixed;
top: 32px;
top: base.rem(32px);
bottom: 0;
left: 0;
right: 0;
}
.Window__contentPadding {
margin: 0.5rem;
// 0.01 is needed to make the scrollbar not appear
// due to rem rendering inaccuracies in IE11.
height: calc(100% - 1.01rem);
}
.Window__contentPadding:after {
height: 0;
}
.Layout__content--scrollable .Window__contentPadding:after {
display: block;
content: '';
height: 0.5rem;
}
.Window__dimmer {
position: fixed;
top: 0;
@@ -48,25 +67,14 @@
pointer-events: none;
}
.Window__toast {
position: fixed;
bottom: 0;
left: 0;
right: 0;
font-size: 12px;
height: 40px;
line-height: 39px;
padding: 0 12px;
background-color: color.scale(base.$color-bg, $lightness: -50%);
color: rgba(255, 255, 255, 0.8);
}
.Window__resizeHandle__se {
position: fixed;
bottom: 0;
right: 0;
width: 20px;
width: base.rem(20px);
height: 20px;
height: base.rem(20px);
cursor: se-resize;
}
@@ -76,6 +84,7 @@
left: 0;
right: 0;
height: 6px;
height: base.rem(6px);
cursor: s-resize;
}
@@ -85,5 +94,6 @@
bottom: 0;
right: 0;
width: 3px;
width: base.rem(3px);
cursor: e-resize;
}
+1 -3
View File
@@ -13,10 +13,7 @@
@include meta.load-css('./atomic/candystripe.scss');
@include meta.load-css('./atomic/color.scss');
@include meta.load-css('./atomic/debug-layout.scss');
@include meta.load-css('./atomic/display.scss');
@include meta.load-css('./atomic/margin.scss');
@include meta.load-css('./atomic/outline.scss');
@include meta.load-css('./atomic/position.scss');
@include meta.load-css('./atomic/text.scss');
// Components
@@ -39,6 +36,7 @@
@include meta.load-css('./components/Slider.scss');
@include meta.load-css('./components/Table.scss');
@include meta.load-css('./components/Tabs.scss');
@include meta.load-css('./components/TextArea.scss');
@include meta.load-css('./components/Tooltip.scss');
// Interfaces
+8 -1
View File
@@ -3,11 +3,13 @@
* SPDX-License-Identifier: MIT
*/
@use './base.scss';
html, body {
box-sizing: border-box;
height: 100%;
margin: 0;
font-size: 12px;
font-size: base.$font-size;
}
html {
@@ -28,22 +30,27 @@ h1, h2, h3, h4, h5, h6 {
display: block;
margin: 0;
padding: 6px 0;
padding: 0.5rem 0;
}
h1 {
font-size: 18px;
font-size: 1.5rem;
}
h2 {
font-size: 16px;
font-size: 1.333rem;
}
h3 {
font-size: 14px;
font-size: 1.167rem;
}
h4 {
font-size: 12px;
font-size: 1rem;
}
td, th {
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use 'sass:color';
@use 'sass:meta';
@use '../colors.scss' with (
$primary: #ad2350,
$fg-map-keys: (),
$bg-map-keys: (),
);
@use '../base.scss' with (
$color-bg: #2a314a,
$color-bg-grad-spread: 6%,
$border-radius: 2px,
);
.theme-abductor {
// Atomic classes
@include meta.load-css('../atomic/color.scss');
// Components
@include meta.load-css('../components/Button.scss', $with: (
'color-default': colors.$primary,
'color-disabled': #363636,
'color-selected': #465899,
'color-caution': #be6209,
'color-danger': #9a9d00,
));
@include meta.load-css('../components/Input.scss', $with: (
'border-color': #404b6e,
));
@include meta.load-css('../components/NoticeBox.scss', $with: (
'color-background': #a82d55,
));
@include meta.load-css('../components/NumberInput.scss', $with: (
'border-color': #404b6e,
));
@include meta.load-css('../components/ProgressBar.scss', $with: (
'color-background': rgba(0, 0, 0, 0.5),
));
@include meta.load-css('../components/Section.scss');
@include meta.load-css('../components/Tooltip.scss', $with: (
'color-background': #a82d55,
));
// Layouts
@include meta.load-css('../layouts/Layout.scss');
@include meta.load-css('../layouts/Window.scss');
@include meta.load-css('../layouts/TitleBar.scss', $with: (
'color-background': #9e1b46,
));
.Layout__content {
background-image: none;
}
}
@@ -14,7 +14,7 @@
@use '../base.scss' with (
$color-bg: #117039,
$color-bg-grad-spread: 0%,
$border-radius: 0px,
$border-radius: 0,
);
//Made for the roulette table, probably requires a bunch of manual hacks to work for anything else
@@ -31,7 +31,7 @@
'color-danger': #9a9d00,
));
@include meta.load-css('../components/NumberInput.scss', $with: (
'border-color': #FFFFFF,
'border-color': #fff,
));
@include meta.load-css('../components/ProgressBar.scss', $with: (
'color-background': rgba(0, 0, 0, 0.5),
@@ -46,8 +46,6 @@
));
.Button {
border-color: #FFFFFF;
border-width: 2px;
border-style: solid;
border: base.em(2px) solid #fff;
}
}
@@ -14,7 +14,6 @@
@use '../base.scss' with (
$color-bg: #121b12,
$color-bg-grad-spread: 0%,
$border-radius: 2px,
);
.theme-hackerman {
@@ -46,10 +45,10 @@
.Button {
font-family: monospace;
border-width: 2px;
border-width: base.em(2px);
border-style: outset;
border-color: #00AA00;
outline: 1px solid rgb(0, 122, 0);
outline: base.em(1px) solid rgb(0, 122, 0);
}
.candystripe:nth-child(odd) {
@@ -14,7 +14,6 @@
@use '../base.scss' with (
$color-bg: #1b3443,
$color-bg-grad-spread: 6%,
$border-radius: 2px,
);
.theme-malfunction {
@@ -14,7 +14,6 @@ $nanotrasen: #384e68;
);
@use '../base.scss' with (
$color-bg: color.scale($nanotrasen, $lightness: -45%),
$border-radius: 2px,
);
.theme-ntos {
+159 -40
View File
@@ -1,52 +1,171 @@
/**
* @file
* @copyright 2020 Paul Bruner
* @license MIT
* Copyright (c) 2020 Paul Bruner
* SPDX-License-Identifier: MIT
*/
@use 'sass:color';
@use 'sass:meta';
@use 'sass:color';
@use 'sass:meta';
@use '../colors.scss' with (
$primary: #ffffff,
$fg-map-keys: (),
$bg-map-keys: (),
);
@use '../base.scss' with (
$color-fg: rgb(0,0,0),
$color-bg: rgb(255, 255, 255),
$color-bg-grad-spread: 0%,
$border-radius: 0px,
);
@use '../colors.scss' with (
$primary: #ffffff,
$bg-lightness: -25%,
$fg-lightness: -10%,
// Commenting out color maps will adjust all colors based on the lightness
// settings above, but will add extra 10KB to the theme.
$fg-map-keys: (),
$bg-map-keys: (),
);
@use '../base.scss' with (
$color-fg: #000000,
$color-bg: #ffffff,
$color-bg-grad-spread: 0%,
);
// A fat warning to anyone who wants to use this: this only half works.
// It was made almost purely for the nuke ui, and requires a good amount of manual hacks to get it working as intended.
.theme-paper {
// Atomic classes
@include meta.load-css('../atomic/color.scss');
$font-size: 24px;
// Components
@include meta.load-css('../components/Tabs.scss');
// A fat warning to anyone who wants to use this: this only half works.
// It was made almost purely for the nuke ui, and requires a good amount of manual hacks to get it working as intended.
.theme-paper {
// Atomic classes
@include meta.load-css('../atomic/color.scss');
// Components
@include meta.load-css('../components/Tabs.scss');
@include meta.load-css('../components/Section.scss', $with: (
'color-background': rgba(0, 0, 0, 0.1),
'color-shadow': rgba(0, 0, 0, 0.2),
));
@include meta.load-css('../components/Button.scss', $with: (
'color-default': #E8E4C9,
'color-disabled': #363636,
'color-selected': #9d0808,
'color-caution': #be6209,
'color-danger': #9a9d00,
'color-transparent-text': rgba(0, 0, 0, 0.5),
));
@include meta.load-css('../components/Button.scss', $with: (
'color-default': #E8E4C9,
'color-disabled': #363636,
'color-selected': #9d0808,
'color-caution': #be6209,
'color-danger': #9a9d00,
));
// Layouts
@include meta.load-css('../layouts/Layout.scss');
// Layouts
@include meta.load-css('../layouts/Layout.scss');
@include meta.load-css('../layouts/Window.scss');
@include meta.load-css('../layouts/TitleBar.scss', $with: (
'color-text': rgba(0, 0, 0, 0.75),
'color-background': base.$color-bg,
'color-shadow-core': rgba(0, 0, 0, 0.25),
));
@include meta.load-css('../layouts/Window.scss');
.PaperInput {
position: relative;
display: inline-block;
width: 120px;
border: none;
background: transparent;
border-bottom: 1px solid #000;
outline: none;
background-color:rgba(255, 255, 62, 0.8);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
overflow: visible;
}
@include meta.load-css('../layouts/TitleBar.scss', $with: (
'color-background': #ffffff,
));
.PaperInput__baseline {
display: inline-block;
color: transparent;
}
.Layout__content {
background-image: none;
}
.PaperInput__input {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 0;
outline: 0;
width: 100%;
font-size: 12px;
line-height: 17px;
height: 17px;
margin: 0;
padding: 0 6px;
font-family: Verdana, sans-serif;
background-color: transparent;
color: #fff;
color: inherit;
}
&:-ms-input-placeholder {
font-style: italic;
color: #777;
color: rgba(255, 255, 255, 0.45);
}
}
.Layout__content {
background-image: none;
}
.Window {
background-image: none;
color: base.$color-fg;
}
.paper-text {
input:disabled {
position: relative;
display: inline-block;
border: none;
background: transparent;
border-bottom: 1px solid #000;
outline: none;
background-color:rgba(255, 255, 62, 0.8);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
overflow: visible;
}
input {
position: relative;
display: inline-block;
border: none;
background: transparent;
border-bottom: 1px solid #000;
outline: none;
background-color:rgba(255, 255, 62, 0.8);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
overflow: visible;
}
}
.paper-field {
position: relative;
display: inline-block;
border: none;
background: transparent;
border-bottom: 1px solid #000;
outline: none;
background-color:rgba(255, 255, 62, 0.8);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
overflow: visible;
input:disabled {
position: relative;
display: inline-block;
border: none;
background: transparent;
border-bottom: 1px solid #000;
outline: none;
background-color:rgba(255, 255, 62, 0.8);
padding: 0 4px;
margin-right: 2px;
line-height: 17px;
overflow: visible;
}
}
}
+3 -5
View File
@@ -14,7 +14,7 @@
@use '../base.scss' with (
$color-bg: #E8E4C9,
$color-bg-grad-spread: 0%,
$border-radius: 0px,
$border-radius: 0,
);
// A fat warning to anyone who wants to use this: this only half works.
@@ -46,10 +46,8 @@
.Button {
font-family: monospace;
color: #161613;
border-width: 8px;
border-style: outset;
border-color: #E8E4C9;
outline: 3px solid #161613;
border: base.em(2px) outset #E8E4C9;
outline: base.em(1px) solid #161613;
}
.Layout__content {
@@ -14,7 +14,6 @@
@use '../base.scss' with (
$color-bg: #550202,
$color-bg-grad-spread: 6%,
$border-radius: 2px,
);
.theme-syndicate {