WIP
parent
6f4cd8bff5
commit
e43c5312fc
@ -1,6 +1,6 @@
|
||||
/* NOTE: The 'glo-bal' filename of this file is a workaround to deal with `insert-module-globals` incorrectly detecting this file to contain a reference to a `g lobal` variable (due to the icssify-generated class names including the filename) */
|
||||
|
||||
.uilibComponent {
|
||||
.uilibThemedElement {
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
row-gap: 2px;
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
@ -0,0 +1,138 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const useMeasure = require("react-use-measure");
|
||||
const syncpipe = require("syncpipe");
|
||||
const defaultValue = require("default-value");
|
||||
const clamp = require("clamp");
|
||||
|
||||
const useTheme = require("../../util/themeable");
|
||||
const defaultStyle = require("./style.css");
|
||||
const childrenWithProps = require("../../util/children-with-props");
|
||||
const generateGridTemplateString = require("../../util/generate-grid-template-string");
|
||||
const useIndexedState = require("../../util/use-indexed-state");
|
||||
const sum = require("../../util/sum");
|
||||
|
||||
const MINIMUM_PANE_SIZE = 16;
|
||||
|
||||
// FIXME: Logic to constrain resizing when increasing a pane's size further would make the paneset exceed its size boundaries
|
||||
// FIXME: Disable selection globally during dragging
|
||||
|
||||
function selectSmallestSize(sizes) {
|
||||
console.log("sizes", sizes);
|
||||
if (sizes.some((size) => size != null && isNaN(size))) {
|
||||
throw new Error(`NaN encountered!`);
|
||||
}
|
||||
|
||||
return syncpipe(sizes, [
|
||||
(_) => _.filter((size) => size != null),
|
||||
(_) => (_.length > 0)
|
||||
? Math.min(... _)
|
||||
: undefined
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = function PaneSet({ vertical, horizontal, children }) {
|
||||
let { withTheme } = useTheme({ control: "paneSet", defaultStyle });
|
||||
let [ boundsRef, bounds ] = useMeasure({ debounce: 10 });
|
||||
let [ newSizes, setNewSize, resetNewSizes ] = useIndexedState();
|
||||
// FIXME: How to deal with new children? Reset all newSizes?
|
||||
|
||||
let direction = (vertical === true)
|
||||
? "vertical"
|
||||
: "horizontal";
|
||||
|
||||
let ownSize = (direction === "vertical")
|
||||
? bounds.height
|
||||
: bounds.width;
|
||||
|
||||
let childrenArray = React.Children.toArray(children);
|
||||
|
||||
let minimumSizes = React.useMemo(() => {
|
||||
return childrenArray.map((child) => {
|
||||
return selectSmallestSize([ child.props.minimumSize, MINIMUM_PANE_SIZE ]);
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: Detect when this fails
|
||||
let autoPaneMinimumSize = syncpipe(childrenArray, [
|
||||
(_) => _.findIndex((child) => child.resizable !== true),
|
||||
(_) => minimumSizes[_]
|
||||
]);
|
||||
|
||||
let maximumSizes = React.useMemo(() => {
|
||||
let paneSizes = childrenArray.map((child, i) => {
|
||||
// FIXME: Require initialSize when resizable
|
||||
return defaultValue(newSizes[i], child.props.initialSize);
|
||||
});
|
||||
|
||||
return childrenArray.map((_, ownIndex) => {
|
||||
let otherPaneSizes = syncpipe(paneSizes, [
|
||||
(_) => _.filter((size, i) => size != null && i !== ownIndex),
|
||||
(_) => sum(_)
|
||||
]);
|
||||
|
||||
return clamp(ownSize - otherPaneSizes - autoPaneMinimumSize, 0, Infinity);
|
||||
});
|
||||
}, [ childrenArray, newSizes, ownSize ]);
|
||||
|
||||
let autoSizeSeen = 0;
|
||||
|
||||
// NOTE: Not using React.Children.map because it will ignore null return values
|
||||
let handleSides = childrenArray.map((child) => {
|
||||
if (child.props.resizable === true) {
|
||||
if (autoSizeSeen > 0) {
|
||||
return (direction === "vertical")
|
||||
? "n" // north, top
|
||||
: "w"; // west, left
|
||||
} else {
|
||||
return (direction === "vertical")
|
||||
? "s" // south, bottom
|
||||
: "e"; // east, right
|
||||
}
|
||||
} else {
|
||||
autoSizeSeen += 1;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
let gridTemplateProperty = (direction === "vertical")
|
||||
? "gridTemplateRows"
|
||||
: "gridTemplateColumns";
|
||||
|
||||
let gridTemplateString = syncpipe(handleSides, [
|
||||
(_) => _.map((handle) => handle != null ? "auto" : 1),
|
||||
(_) => generateGridTemplateString(_)
|
||||
]);
|
||||
|
||||
let style = {
|
||||
[gridTemplateProperty]: gridTemplateString
|
||||
};
|
||||
|
||||
if (autoSizeSeen !== 1) {
|
||||
// We don't allow *multiple* auto-sized panes, to avoid weird behaviour where the auto-layouted panes may not end up exactly where the resize handle is
|
||||
throw new Error(`Exactly one (center) pane must be non-resizable!`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={boundsRef} className={withTheme("uilibComponent", "paneSet", `direction-${direction}`)} style={style}>
|
||||
{childrenWithProps(children, (child, i) => {
|
||||
let handleSide = handleSides[i];
|
||||
|
||||
return {
|
||||
onResized: (newSize) => {
|
||||
setNewSize(i, newSize);
|
||||
},
|
||||
maximumSize: selectSmallestSize([ child.props.maximumSize, maximumSizes[i] ]),
|
||||
// Note that we shouldn't override an explicitly user-configured handleSide
|
||||
... (handleSide == null)
|
||||
? {}
|
||||
: {
|
||||
handleSide: defaultValue(child.props.handleSide, handleSide),
|
||||
direction: direction
|
||||
}
|
||||
};
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
:import("../pane/style.css") { pane: pane; }
|
||||
|
||||
.paneSet {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.direction-horizontal {
|
||||
.pane {
|
||||
height: 100%;
|
||||
|
||||
/* FIXME: This should really be & > ..., but that isn't allowed by the parser */
|
||||
:global .react-resizable {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.direction-vertical {
|
||||
.pane {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const { ResizableBox } = require("react-resizable");
|
||||
const defaultValue = require("default-value");
|
||||
|
||||
const useDisableSelection = require("../../util/use-disable-selection");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const defaultStyle = require("./style.css");
|
||||
|
||||
function Handle({ ... props }) {
|
||||
let { withTheme } = useTheme({ control: "pane", defaultStyle });
|
||||
|
||||
// NOTE: We need to forward all props to make the mouse events work, which react-resizable internally adds to the component
|
||||
return (
|
||||
<div className={withTheme("handleHitbox")} {... props}>
|
||||
<div className={withTheme("handle")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Only exact pixel values currently supported for initialSize, add some sort of percentage support in the future?
|
||||
// FIXME: Increase handle hitbox size
|
||||
// FIXME: `Resizable` wrapper element
|
||||
|
||||
// NOTE: onResized and direction are internal, handleSide too but it can be overwritten
|
||||
module.exports = function Pane({ onResized, resizable, initialSize, minimumSize, maximumSize, direction, handleSide, children }) {
|
||||
let { withTheme } = useTheme({ control: "pane", defaultStyle });
|
||||
let { disableGlobalSelection, enableGlobalSelection } = useDisableSelection();
|
||||
|
||||
// FIXME: Validate inputs
|
||||
|
||||
let sizeProps = (direction === "vertical")
|
||||
? {
|
||||
height: initialSize,
|
||||
minConstraints: [ 0, defaultValue(minimumSize, 0) ],
|
||||
maxConstraints: [ Infinity, defaultValue(maximumSize, Infinity) ],
|
||||
}
|
||||
: {
|
||||
width: initialSize,
|
||||
minConstraints: [ defaultValue(minimumSize, 0), 0 ],
|
||||
maxConstraints: [ defaultValue(maximumSize, Infinity), Infinity ],
|
||||
};
|
||||
|
||||
let axisProp = (direction === "vertical")
|
||||
? "y"
|
||||
: "x";
|
||||
|
||||
function handleOnResizeStop(node, event) {
|
||||
let newSize = (direction === "vertical")
|
||||
? event.size.height
|
||||
: event.size.width;
|
||||
|
||||
onResized(newSize);
|
||||
enableGlobalSelection();
|
||||
}
|
||||
|
||||
function handleOnResizeStart() {
|
||||
disableGlobalSelection();
|
||||
}
|
||||
|
||||
// NOTE: Needed to prevent react-resizable from somehow breaking hooks inside of Handle
|
||||
function handleFunction() {
|
||||
return <Handle />;
|
||||
}
|
||||
|
||||
// FIXME: Do we still need the contentWrapper, without the bounds-measuring logic?
|
||||
let content = (
|
||||
<div className={withTheme("contentWrapper")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={withTheme("uilibComponent", "pane", `handleSide-${handleSide}`)}>
|
||||
{(resizable === true)
|
||||
? <ResizableBox handle={handleFunction} resizeHandles={[ handleSide ]} axis={axisProp} onResizeStop={handleOnResizeStop} onResizeStart={handleOnResizeStart} {... sizeProps}>
|
||||
{content}
|
||||
</ResizableBox>
|
||||
: content
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,88 @@
|
||||
/* FIXME: Move these variables to theme variables eventually */
|
||||
$handleSize: 2px;
|
||||
$hitboxPadding: 5px;
|
||||
|
||||
.pane {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handleHitbox {
|
||||
position: absolute;
|
||||
/* background-color: red; */
|
||||
}
|
||||
|
||||
.handleSide-n, .handleSide-s {
|
||||
.handleHitbox {
|
||||
cursor: ns-resize;
|
||||
padding: $hitboxPadding 0;
|
||||
}
|
||||
|
||||
.handle {
|
||||
height: $handleSize;
|
||||
}
|
||||
}
|
||||
|
||||
.handleSide-e, .handleSide-w {
|
||||
.handleHitbox {
|
||||
cursor: ew-resize;
|
||||
padding: 0 $hitboxPadding;
|
||||
}
|
||||
|
||||
.handle {
|
||||
width: $handleSize;
|
||||
}
|
||||
}
|
||||
|
||||
.handleSide-n {
|
||||
/* north / top */
|
||||
padding-top: $handleSize;
|
||||
|
||||
.handleHitbox {
|
||||
top: calc(0px - $hitboxPadding);
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.handleSide-s {
|
||||
/* south / bottom */
|
||||
padding-bottom: $handleSize;
|
||||
|
||||
.handleHitbox {
|
||||
bottom: calc(0px - $hitboxPadding);
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.handleSide-e {
|
||||
/* east - right */
|
||||
padding-right: $handleSize;
|
||||
|
||||
.handleHitbox {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: calc(0px - $hitboxPadding);
|
||||
}
|
||||
}
|
||||
|
||||
.handleSide-w {
|
||||
/* west - left */
|
||||
padding-left: $handleSize;
|
||||
|
||||
.handleHitbox {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(0px - $hitboxPadding);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const useTheme = require("../../util/themeable");
|
||||
const defaultStyle = require("./style.css");
|
||||
const generateGridItemStyle = require("../../util/generate-grid-item-style");
|
||||
|
||||
module.exports = function StatusIndicator({ x, y, status, children }) {
|
||||
let { withTheme } = useTheme({ control: "statusIndicator", defaultStyle });
|
||||
|
||||
return (
|
||||
<div className={withTheme("uilibComponent", "statusIndicator", `status-${status}`)} style={generateGridItemStyle({ x, y })}>
|
||||
<div className={withTheme("content")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
.statusIndicator {
|
||||
composes: centerContent from "../shared.css";
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 4px; /* FIXME: Why do we have this? */
|
||||
}
|
||||
|
||||
.status-positive {
|
||||
background-color: rgb(0, 93, 9);
|
||||
}
|
||||
|
||||
.status-negative {
|
||||
background-color: rgb(106, 0, 0);
|
||||
}
|
||||
|
||||
.status-neutral {
|
||||
background-color: #5a5a5a;
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
$darkGray: rgb(34, 34, 34);
|
||||
$lessDarkGray: rgb(44, 44, 44);
|
||||
$hoverColor: #e5e5e5;
|
||||
$activeColor: #c5dae5;
|
||||
|
||||
$white: white;
|
||||
$barColor: #edf5f5;
|
||||
$lightGray: silver;
|
||||
|
||||
/* :export {
|
||||
darkGray: $darkGray;
|
||||
hoverColor: $hoverColor;
|
||||
activeColor: $activeColor;
|
||||
} */
|
||||
|
||||
/* FIXME: Casemap control names */
|
||||
|
||||
body {
|
||||
/* FIXME: Think about whether this belongs in the theme, or whether it should be scoped to some sort of ApplicationFrame instead. */
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.edgeRaise {
|
||||
box-shadow: inset -1px -1px rgba(187, 183, 183, 0.7),
|
||||
inset 1px 1px rgba(206, 206, 206, 0.3);
|
||||
}
|
||||
|
||||
.edgeLower {
|
||||
box-shadow: inset 1px 1px rgba(187, 183, 183, 0.7),
|
||||
inset -1px -1px rgba(206, 206, 206, 0.3);
|
||||
}
|
||||
|
||||
.bar {
|
||||
composes: edgeRaise;
|
||||
background-color: $barColor;
|
||||
}
|
||||
|
||||
.uilibComponent {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.list_list {
|
||||
/* color: red; */
|
||||
}
|
||||
|
||||
.list_item {
|
||||
&:nth-child(odd) {
|
||||
/* background-color: rgb(45, 45, 45); */
|
||||
/* background-color: rgb(51, 51, 51); */
|
||||
background-color: rgb(57, 57, 60);
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: rgb(27, 27, 27);
|
||||
}
|
||||
|
||||
&.list_item_selected {
|
||||
/* FIXME: Find a better color for this. */
|
||||
/* background-color: rgb(38, 38, 42); */
|
||||
background-color: blue; /* FIXME: Remove testing color */
|
||||
}
|
||||
}
|
||||
|
||||
.menu_menuBar, .ribbon_ribbon {
|
||||
composes: bar;
|
||||
}
|
||||
|
||||
.menu_menuBar > .menu_item {
|
||||
&:hover {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
|
||||
&.menu_item_selected, &menu_item_directPress {
|
||||
background-color: $activeColor;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_menu {
|
||||
background-color: $barColor;
|
||||
box-shadow: 1px 1px 2px rgb(54, 54, 54);
|
||||
|
||||
& > .menu_item:hover {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 1px solid rgb(78, 78, 78);
|
||||
}
|
||||
}
|
||||
|
||||
.menu_divider {
|
||||
border-bottom: 1px solid rgb(78, 78, 78);
|
||||
}
|
||||
|
||||
.button_button {
|
||||
background-color: #d0d0d0;
|
||||
/* background-color: magenta; */
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@include edge-lower;
|
||||
background-color: $activeColor;
|
||||
}
|
||||
|
||||
&.button_selected {
|
||||
background-color: $darkGray;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbonBox_box {
|
||||
border-left: 1px solid rgba(30, 27, 27, 0.7);
|
||||
border-right: 1px solid rgba(135, 131, 131, 0.3);
|
||||
}
|
||||
|
||||
.ribbonBox_label {
|
||||
background-color: #f3f1fa;
|
||||
}
|
||||
|
||||
.progressBar_bar {
|
||||
border: 1px solid rgb(32, 32, 32);
|
||||
/* background-color: rgb(64, 64, 70); */
|
||||
background-color: rgb(43, 43, 47);
|
||||
}
|
||||
|
||||
.progressBar_fill {
|
||||
background-color: rgb(54, 135, 18);
|
||||
}
|
||||
|
||||
.pane_handle {
|
||||
background-color: rgb(245, 245, 245);
|
||||
composes: edgeRaise;
|
||||
}
|
||||
|
||||
.ribbon_ribbon {
|
||||
/* ... */
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
// NOTE: Only use this where absolutely needed! Normally, the context API should be preferred where possible.
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = function childrenWithProps(children, mapper) {
|
||||
return React.Children.map(children, (child, i) => {
|
||||
let extraProps = (typeof mapper === "function")
|
||||
? mapper(child, i)
|
||||
: mapper;
|
||||
|
||||
if (extraProps != null) {
|
||||
// FIXME: Do we need to check React.isValidElement here, to deal with eg. text nodes? Or is that handled internally by React.cloneElement?
|
||||
return React.cloneElement(child, extraProps);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
function normalizeGridCellSize(value) {
|
||||
if (typeof value === "number") {
|
||||
return `${value}fr`;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function generateGridTemplateString(values) {
|
||||
return values
|
||||
.map((value) => normalizeGridCellSize(value))
|
||||
.join(" ");
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function sum(numbers) {
|
||||
return numbers.reduce((total, number) => total + number, 0);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = function useDisableSelection() {
|
||||
let body = document.querySelector("body");
|
||||
let lastSelectSetting = React.useRef();
|
||||
|
||||
return {
|
||||
disableGlobalSelection: function () {
|
||||
if (body.style.userSelect !== "none") {
|
||||
lastSelectSetting.current = body.style.userSelect;
|
||||
body.style.userSelect = "none";
|
||||
}
|
||||
},
|
||||
enableGlobalSelection: function () {
|
||||
if (body.style.userSelect === "none" && lastSelectSetting.current != null) {
|
||||
body.style.userSelect = lastSelectSetting.current;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const timm = require("timm");
|
||||
|
||||
module.exports = function useIndexedState(initialState = []) {
|
||||
let [ state, setState ] = React.useState(initialState);
|
||||
|
||||
function setIndexedState(index, value) {
|
||||
setState((oldArray) => {
|
||||
let evaluatedValue = (typeof value === "function")
|
||||
? value(oldArray[index])
|
||||
: value;
|
||||
|
||||
return timm.replaceAt(oldArray, index, evaluatedValue);
|
||||
});
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
setState((oldArray) => {
|
||||
return oldArray.map(() => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
state,
|
||||
setIndexedState,
|
||||
resetState
|
||||
];
|
||||
};
|
Loading…
Reference in New Issue