master
Sven Slootweg 4 years ago
parent 7dff6bfdef
commit 6f4cd8bff5

@ -1,6 +1,8 @@
# FIXME
- Figure out a way to make this automatically load the correct PostCSS plugins, when used with icssify (since we probably don't want to pre-compile the CSS?)
- Sort out naming consistency for a) control names and b) classification of related elements (as their own controls? like for MenuItem, which is currently a part of Menu)
- Move radius etc. to the theme
# Name ideas
@ -8,7 +10,10 @@
# Feature ideas
SetIconSource wrapper, like SetTheme, for providing a custom Icon component (eg. when using an icon font instead of images), and some way to have icon fallbacks
- SetIconSource wrapper, like SetTheme, for providing a custom Icon component (eg. when using an icon font instead of images), and some way to have icon fallbacks
- `_$override` suffix for theme class names, that disables the default themeing when used?
- Mark ribbon boxes as 'collapsible' to have them disappear into the overflow menu first, when not all boxes fit onto the screen (it's not always the boxes at the end that are the least important)
- Some sort of shared argument-processing abstraction for components, which takes custom validation rules and handles stuff like grid item style generation as a pre-processing step
# Context design rules

@ -6,18 +6,31 @@ const asExpression = require("as-expression");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
const contexts = require("../../contexts");
const useGuaranteedMemo = require("../../util/use-guaranteed-memo");
const composeHandlers = require("../../util/compose-handlers");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
module.exports = function ButtonSet({ direction, choice, children }) {
module.exports = function ButtonSet({ x, y, horizontal, vertical, choice, onSelect, children }) {
let { withTheme } = useTheme({ control: "buttonSet", defaultStyle });
let [ selectedItem, setSelectedItem ] = React.useState();
// direction: horizontal, vertical
// FIXME: Validate direction
let handleOnClick = useGuaranteedMemo(() => {
return composeHandlers([
onSelect,
setSelectedItem
]);
}, [ setSelectedItem ]);
// FIXME: Validate horizontal, vertical - require one of
// FIXME: Do below as post-processing in validation (like tags)
let direction = (vertical === true)
? "vertical"
: "horizontal";
let wrappedChildren = asExpression(() => {
if (choice === true) {
return (
<contexts.button_onClick.Provider value={setSelectedItem}>
<contexts.button_onClick.Provider value={handleOnClick}>
<contexts.button_selectedID.Provider value={selectedItem}>
{children}
</contexts.button_selectedID.Provider>
@ -29,7 +42,7 @@ module.exports = function ButtonSet({ direction, choice, children }) {
});
return (
<div className={withTheme("buttonSet", `direction-${direction}`)}>
<div className={withTheme("buttonSet", `direction-${direction}`)} style={generateGridItemStyle({ x, y })}>
{wrappedChildren}
</div>
);

@ -1,11 +1,16 @@
.buttonSet {
display: inline-flex;
composes: combinedButton from "../shared.css";
}
.direction-horizontal {
grid-auto-columns: 1fr;
flex-direction: row;
/* composes: combinedHorizontal from "../shared.css"; */
/* grid-auto-columns: 1fr; */
}
.direction-vertical {
grid-auto-rows: 1fr;
flex-direction: column;
/* composes: combinedVertical from "../shared.css"; */
/* grid-auto-rows: 1fr; */
}

@ -7,33 +7,42 @@ const useTheme = require("../../util/themeable");
const useID = require("../../util/use-id");
const contexts = require("../../contexts");
const composeHandlers = require("../../util/compose-handlers");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
const Icon = require("../icon");
module.exports = function Button({ type, onClick, children, icon, default: default_ }) {
module.exports = function Button({ x, y, id, type, onClick, children, icon, default: default_ }) {
// TODO: Validate type?
// TODO: How does icon + custom HTML children end up looking? Should we use a grid to ensure that the icon is always displayed on the left in its own column?
let { withTheme } = useTheme({ control: "button", defaultStyle });
let id = useID();
let ownID = useID(id);
let selectedID = React.useContext(contexts.button_selectedID);
let capturedOnClick = React.useContext(contexts.button_onClick);
React.useEffect(() => {
if (capturedOnClick != null && default_ === true) {
// This is a default selection in a choice list of some sort, eg. a button-set - so we simulate a click event.
capturedOnClick(id);
capturedOnClick(ownID);
}
}, [ capturedOnClick, id ]);
}, [ capturedOnClick, ownID ]);
let isSelected = (id === selectedID);
let isSelected = (ownID === selectedID);
let onClickAll = composeHandlers([
capturedOnClick,
onClick
]);
let typeClass = (type != null)
? `type-${type}`
: undefined;
return (
<button className={withTheme("button", `type-${type}`, { selected: isSelected })} onClick={() => onClickAll(id)}>
<button
className={withTheme("button", typeClass, { selected: isSelected })}
onClick={() => onClickAll(ownID)}
style={generateGridItemStyle({ x, y })}
>
<span className={withTheme("buttonContents")}>
{(icon != null)
? <Icon icon={icon} />

@ -1,13 +1,17 @@
:import("../icon/style.css") {
icon: icon;
}
:import("../icon/style.css") { icon: icon; }
.button {
composes: centerContent from "../shared.css";
box-sizing: border-box;
padding: 3px;
font-size: 12px;
border-radius: 2px;
.icon {
display: block;
margin: 0px auto 7px auto;
width: 28px;
height: 28px;
}
}
.buttonContents {

@ -2,4 +2,5 @@
.uilibComponent {
font-family: sans-serif;
font-size: 12px;
}

@ -0,0 +1,73 @@
"use strict";
const React = require("react");
const defaultValue = require("default-value");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
function normalizeGridCellSize(value) {
if (typeof value === "number") {
return `${value}fr`;
} else {
return value;
}
}
function generateGridTemplateString(values) {
return values
.map((value) => normalizeGridCellSize(value))
.join(" ");
}
module.exports = function Grid({ x, y, rows, columns, rowGap, columnGap, children }) {
let { withTheme } = useTheme({ control: "grid", defaultStyle });
let rows_ = defaultValue(rows, [1]);
let columns_ = defaultValue(columns, [1]);
let style = {
gridTemplateRows: generateGridTemplateString(rows_),
gridTemplateColumns: generateGridTemplateString(columns_),
gridRowGap: rowGap,
gridColumnGap: columnGap,
... generateGridItemStyle({ x, y })
};
// FIXME: Context for x/y
return (
<div className={withTheme("grid")} style={style}>
{children}
</div>
);
// return (
// <div className="ribbonGrid" style={style}>
// {childrenWithProps(props.children, (item) => {
// let itemStyle = {};
// if (item.props.x != null) {
// itemStyle.gridColumnStart = item.props.x + 1;
// if (item.props.cellWidth != null) {
// itemStyle.gridColumnEnd = itemStyle.gridColumnStart + item.props.cellWidth;
// }
// }
// if (item.props.y != null) {
// itemStyle.gridRowStart = item.props.y + 1;
// if (item.props.cellHeight != null) {
// itemStyle.gridRowEnd = itemStyle.gridRowStart + item.props.cellHeight;
// }
// }
// return {
// style: itemStyle
// };
// })}
// </div>
// );
};

@ -0,0 +1,5 @@
.grid {
display: grid;
row-gap: 2px;
column-gap: 2px;
}

@ -3,7 +3,6 @@
}
.item {
font-size: 12px;
padding: 4px 6px;
.icon {

@ -5,10 +5,11 @@ const React = require("react");
const defaultStyle = require("./style.css");
const useTheme = require("../../util/themeable");
const ListItemContext = require("../../contexts/list-item");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
// FIXME: Track full collapsing state throughout the tree
module.exports = function List({ children, onPick, onSelect }) {
module.exports = function List({ x, y, children, onPick, onSelect }) {
let [ selectedItem, setSelectedItem ] = React.useState([]);
let { withTheme } = useTheme({ control: "list", defaultStyle });
@ -31,7 +32,7 @@ module.exports = function List({ children, onPick, onSelect }) {
return (
<ListItemContext.Provider value={itemContext}>
<div className={withTheme("list")}>
<div className={withTheme("list")} style={generateGridItemStyle({ x, y })}>
{children}
</div>
</ListItemContext.Provider>

@ -0,0 +1,12 @@
"use strict";
const React = require("react");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
module.exports = function MenuDivider() {
let { withTheme } = useTheme({ control: "menu", defaultStyle });
return <hr className={withTheme("divider")} />;
};

@ -0,0 +1,3 @@
.divider {
margin: 4px 4px;
}

@ -18,9 +18,3 @@
.menu > .item {
padding: 4px 9px;
}
.menu hr {
color: none;
border: none;
margin: 4px 4px;
}

@ -0,0 +1,18 @@
"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 ProgressBar({ x, y, progress, color }) {
// FIXME: Make color a theme thing
let { withTheme } = useTheme({ control: "progressBar", defaultStyle });
return (
<div className={withTheme("bar")} style={generateGridItemStyle({ x, y })}>
<div className={withTheme("fill")} style={{width: `${progress * 100}%`, backgroundColor: color}} />
</div>
);
};

@ -0,0 +1,11 @@
.bar {
height: 24px;
min-width: 16px;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
}
.fill {
height: 100%;
}

@ -0,0 +1,28 @@
"use strict";
const React = require("react");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
const Button = require("../button");
const ProgressBar = require("../progress-bar");
module.exports = function ProgressButton({ x, y, horizontal, vertical, type, icon, progress, progressColor, children }) {
// FIXME: progressColor in theme, same for custom button style -- maybe make `type` a thing that takes an array, for multiple type classes? to deal with composite controls
let { withTheme } = useTheme({ control: "progressButton", defaultStyle });
let direction = (vertical === true)
? "vertical"
: "horizontal";
return (
<div className={withTheme("progressButton", `direction-${direction}`)} style={generateGridItemStyle({ x, y })}>
<Button type={type} icon={icon}>
{children}
</Button>
<ProgressBar progress={progress} progressColor={progressColor} />
</div>
);
};

@ -0,0 +1,21 @@
:import("../progress-bar/style.css") { progressBar: bar; }
.progressButton {
composes: combinedButton from "../shared.css";
display: inline-grid;
box-sizing: border-box;
border-radius: 4px;
overflow: hidden;
.progressBar {
/* border-top: 0; */
}
}
.direction-horizontal {
grid-template-columns: auto 1fr;
}
.direction-vertical {
grid-template-rows: 2fr 1fr;
}

@ -0,0 +1,24 @@
"use strict";
const React = require("react");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
module.exports = function RibbonBox({ label, children, width, height }) {
let { withTheme } = useTheme({ control: "ribbonBox", defaultStyle });
// FIXME: How to handle width/height here? Is the current approach correct? Or should we let the user override this through a class?
return (
<div className={withTheme("box")} style={{width: width, height: height}}>
<div className={withTheme("contents")}>
{children}
</div>
<div className={withTheme("label")} title={label}>
{label}
</div>
</div>
);
};

@ -0,0 +1,30 @@
.box {
display: grid;
grid-template-rows: 1fr auto;
margin: 4px 0px;
padding: 0px 8px;
&:first-child {
border-left: none;
}
&:last-child {
border-right: none;
}
}
.label {
padding: 1px 5px;
font-size: .9em;
margin-top: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.contents {
display: flex;
flex-wrap: wrap;
flex-direction: column;
overflow: hidden;
}

@ -4,12 +4,13 @@ const React = require("react");
const defaultStyle = require("./style.css");
const useTheme = require("../../util/themeable");
const generateGridItemStyle = require("../../util/generate-grid-item-style");
module.exports = function Ribbon({ children }) {
module.exports = function Ribbon({ x, y, children }) {
let { withTheme } = useTheme({ control: "ribbon", defaultStyle });
return (
<div className={withTheme("ribbon")}>
<div className={withTheme("ribbon")} style={generateGridItemStyle({ x, y })}>
{children}
</div>
);

@ -1,4 +1,9 @@
:import("../button/style.css") { button: button; }
:import("../button-set/style.css") { buttonSet: buttonSet; }
:import("../text/style.css") { text: text; }
:import("../icon/style.css") { icon: icon; }
:import("../grid/style.css") { grid_class: grid; } /* NOTE: Weird alias to avoid conflicting with `display: grid;` */
:import("../progress-bar/style.css") { progressBar: bar; }
:import("../shared.css") { combinedButton: combinedButton; }
.ribbon {
@ -9,10 +14,34 @@
width: 100%;
height: 100%;
min-width: 16px;
/* .icon {
display: block;
margin: 0px auto 7px auto;
width: 28px;
height: 28px;
} */
}
.combinedButton {
display: grid; /* block, not inline */
/* display: flex; block, not inline */
height: 100%;
}
.text, .grid_class {
height: 100%;
}
.buttonSet {
display: flex;
}
.progressBar, .progressButton {
height: 100%;
border-radius: 2px;
}
.progressButton {
display: grid;
}
}

@ -17,7 +17,7 @@
}
.combinedButton {
display: inline-grid;
/* display: inline-flex; */
box-sizing: border-box;
border-radius: 2px;
overflow: hidden;
@ -26,3 +26,11 @@
border-radius: 0px !important;
}
}
.combinedHorizontal {
/* flex-direction: row; */
}
.combinedVertical {
/* flex-direction: column; */
}

@ -0,0 +1,21 @@
"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 Text({ x, y, align, children }) {
let { withTheme } = useTheme({ control: "text", defaultStyle });
let alignClass = (align != null)
? `align-${align}`
: undefined;
return (
<div className={withTheme("text", alignClass)} style={generateGridItemStyle({ x, y })}>
{children}
</div>
);
};

@ -0,0 +1,15 @@
.text {
composes: centerContent from "../shared.css";
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}

@ -2,9 +2,13 @@
module.exports = {
/* Multi-purpose controls */
Grid: require("./controls/grid/index.jsx"),
Text: require("./controls/text/index.jsx"),
Icon: require("./controls/icon/index.jsx"),
Button: require("./controls/button/index.jsx"),
ButtonSet: require("./controls/button-set/index.jsx"),
ProgressBar: require("./controls/progress-bar/index.jsx"),
ProgressButton: require("./controls/progress-button/index.jsx"),
// Text: require("./controls/text.jsx"),
// /* Pane layout */
@ -17,15 +21,15 @@ module.exports = {
Menu: require("./controls/menu/index.jsx"),
MenuBar: require("./controls/menu-bar/index.jsx"),
MenuItem: require("./controls/menu-item/index.jsx"),
// MenuDivider: require("./controls/menu/divider.jsx"),
MenuDivider: require("./controls/menu-divider/index.jsx"),
// /* List */
List: require("./controls/list/index.jsx"),
ListItem: require("./controls/list-item/index.jsx"), // NOTE: This will also be used for other types of lists (eg. dropdowns)
// /* Ribbon */
// Ribbon: require("./controls/ribbon/ribbon.jsx"),
// RibbonBox: require("./controls/ribbon/box.jsx"),
Ribbon: require("./controls/ribbon/index.jsx"),
RibbonBox: require("./controls/ribbon-box/index.jsx"),
// // RibbonItem: require("./ribbon/item.jsx"), // Used internally, but not exported (yet)
// RibbonButton: require("./controls/ribbon/button.jsx"),
// RibbonButtonSet: require("./controls/ribbon/button-set.jsx"),

@ -10,6 +10,11 @@ $activeColor: $darkGray;
/* 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: $darkGray;
}
.edgeRaise {
box-shadow: inset -1px -1px rgba(30, 27, 27, 0.7),
inset 1px 1px rgba(135, 131, 131, 0.3);
@ -25,13 +30,15 @@ $activeColor: $darkGray;
background-color: rgb(60, 62, 66);
}
.uilibComponent {
color: white;
}
.list_list {
/* color: red; */
}
.list_item {
color: white;
&:nth-child(odd) {
/* background-color: rgb(45, 45, 45); */
/* background-color: rgb(51, 51, 51); */
@ -54,8 +61,6 @@ $activeColor: $darkGray;
}
.menu_menuBar > .menu_item {
color: white;
&:hover {
background-color: $hoverColor;
}
@ -78,9 +83,13 @@ $activeColor: $darkGray;
}
}
.menu_divider {
border-bottom: 1px solid rgb(78, 78, 78);
}
.button_button {
// background-color: #38383c;
background-color: magenta;
background-color: #38383c;
// background-color: magenta;
border: none;
&:hover {
@ -96,3 +105,27 @@ $activeColor: $darkGray;
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: #47464b;
color: rgb(238, 238, 238);
}
.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);
}
.ribbon_ribbon {
/* ... */
}

@ -0,0 +1,21 @@
"use strict";
function generateSpecification(coordinates) {
if (coordinates == null) {
return undefined;
} else if (Array.isArray(coordinates)) {
let [ start, end ] = coordinates;
// NOTE: Exclusive -> inclusive conversion for second coordinate
return `${start + 1} / ${end + 2}`;
} else {
return coordinates + 1;
}
}
module.exports = function ({ x, y }) {
return {
gridColumn: generateSpecification(x),
gridRow: generateSpecification(y)
};
};

@ -3,6 +3,7 @@
const React = require("react");
const classnames = require("classnames");
const defaultValue = require("default-value");
const syncpipe = require("syncpipe");
const extendClassnames = require("../extend-classnames");
const ThemeContext = require("./context");
@ -27,21 +28,23 @@ module.exports = function useTheme({ control, defaultStyle }) {
// if (arguments.length > 1) {
// console.warn(`WARNING: More than one argument specified to 'withTheme' call (second argument is ${JSON.stringify(arguments[1])})`);
// }
let mappedClasses = extendClassnames(classes, (className) => {
return [
mapDefaultName(defaultValue(defaultStyle, {}), className),
(theme != null)
? mapThemeName(theme.css, className, rulePrefix)
: null,
mapDefaultName(globalStyle, "uilibComponent"),
(theme != null)
? mapThemeName(theme.css, "uilibComponent", "")
: null,
];
});
return classnames(... mappedClasses);
return syncpipe(classes, [
(_) => _.filter((className) => className != null),
(_) => extendClassnames(_, (className) => {
return [
mapDefaultName(defaultValue(defaultStyle, {}), className),
(theme != null)
? mapThemeName(theme.css, className, rulePrefix)
: null,
mapDefaultName(globalStyle, "uilibComponent"),
(theme != null)
? mapThemeName(theme.css, "uilibComponent", "")
: null,
];
}),
(_) => classnames(..._)
]);
}
return { theme, withTheme };

@ -1,8 +1,11 @@
"use strict";
const defaultValue = require("default-value");
const insecureNanoid = require("nanoid/non-secure").nanoid;
const useGuaranteedMemo = require("./use-guaranteed-memo");
module.exports = function useID() {
return useGuaranteedMemo(() => insecureNanoid());
module.exports = function useID(explicitID) {
let generatedID = useGuaranteedMemo(() => insecureNanoid());
return defaultValue(explicitID, generatedID);
};

Loading…
Cancel
Save