diff --git a/notes.md b/notes.md index 6b2aac9..eba0686 100644 --- a/notes.md +++ b/notes.md @@ -3,6 +3,10 @@ - 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 +- Shared 'stretchable' class that can be composed by all elements which can be height:100%'d in something like a ribbon + - Make this a fixed-name export rather than an auto-generated class name, to make it work across different versions of the library in the future? Maybe also do this for other 'marker classes'? +- Generic "focus" state abstraction, reusable between menus, dropdowns, etc. so that all 'ephemeral controls' behave like one would expect from a native application +- Add React to peerDependencies # Name ideas diff --git a/package.json b/package.json index 41cf1c7..cdf6790 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,16 @@ "@react-hook/debounce": "^3.0.0", "as-expression": "^1.0.0", "assure-array": "^1.0.0", + "clamp": "^1.0.1", "classnames": "^2.2.6", "default-value": "^1.0.0", "flatten": "^1.0.3", "match-value": "^1.1.0", "nanoid": "^3.1.12", - "react": "link:../site-builder/node_modules/react", + "react-resizable": "^2.0.0", "react-use-measure": "^2.0.1", - "syncpipe": "^1.0.0" + "syncpipe": "^1.0.0", + "timm": "^1.7.1" }, "browserify": { "transform": [ diff --git a/src/controls/button-set/index.jsx b/src/controls/button-set/index.jsx index 2243545..d4d709e 100644 --- a/src/controls/button-set/index.jsx +++ b/src/controls/button-set/index.jsx @@ -42,7 +42,7 @@ module.exports = function ButtonSet({ x, y, horizontal, vertical, choice, onSele }); return ( -
+
{wrappedChildren}
); diff --git a/src/controls/button/index.jsx b/src/controls/button/index.jsx index 176feaa..760938e 100644 --- a/src/controls/button/index.jsx +++ b/src/controls/button/index.jsx @@ -11,6 +11,8 @@ const generateGridItemStyle = require("../../util/generate-grid-item-style"); const Icon = require("../icon"); +// FIXME: Mark div as button with aria attributes + 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? @@ -38,8 +40,8 @@ module.exports = function Button({ x, y, id, type, onClick, children, icon, defa : undefined; return ( - +
); }; diff --git a/src/controls/button/style.css b/src/controls/button/style.css index a9fc785..e3d88c7 100644 --- a/src/controls/button/style.css +++ b/src/controls/button/style.css @@ -2,19 +2,21 @@ .button { composes: centerContent from "../shared.css"; + cursor: default; box-sizing: border-box; - padding: 3px; + padding: 4px; border-radius: 2px; .icon { display: block; - margin: 0px auto 7px auto; + margin: 3px auto 7px auto; width: 28px; height: 28px; } } .buttonContents { + display: block; padding: 0px 6px; } diff --git a/src/controls/glo-bal-style.css b/src/controls/glo-bal-style.css index af043a3..bf3e317 100644 --- a/src/controls/glo-bal-style.css +++ b/src/controls/glo-bal-style.css @@ -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; } diff --git a/src/controls/grid/index.jsx b/src/controls/grid/index.jsx index c58cd6a..549137c 100644 --- a/src/controls/grid/index.jsx +++ b/src/controls/grid/index.jsx @@ -6,20 +6,7 @@ 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(" "); -} +const generateGridTemplateString = require("../../util/generate-grid-template-string"); module.exports = function Grid({ x, y, rows, columns, rowGap, columnGap, children }) { let { withTheme } = useTheme({ control: "grid", defaultStyle }); @@ -38,7 +25,7 @@ module.exports = function Grid({ x, y, rows, columns, rowGap, columnGap, childre // FIXME: Context for x/y return ( -
+
{children}
); diff --git a/src/controls/grid/style.css b/src/controls/grid/style.css index 53abce5..9b02509 100644 --- a/src/controls/grid/style.css +++ b/src/controls/grid/style.css @@ -1,5 +1,7 @@ .grid { display: grid; + width: 100%; + height: 100%; row-gap: 2px; column-gap: 2px; } diff --git a/src/controls/icon/index.jsx b/src/controls/icon/index.jsx index 33472f0..0e13e8b 100644 --- a/src/controls/icon/index.jsx +++ b/src/controls/icon/index.jsx @@ -11,6 +11,6 @@ module.exports = function Icon({ icon }) { // FIXME: Don't hardcode icon base path, get it from theme return ( - + ); }; diff --git a/src/controls/list-item/index.jsx b/src/controls/list-item/index.jsx index 82e31f6..b61d62e 100644 --- a/src/controls/list-item/index.jsx +++ b/src/controls/list-item/index.jsx @@ -66,7 +66,7 @@ module.exports = function ListItem({ label, id, children }) { return ( {/* FIXME: item_selected capitalization */} -
+
{(children != null) ? : } diff --git a/src/controls/list-item/style.css b/src/controls/list-item/style.css index 74f13b9..6d74b55 100644 --- a/src/controls/list-item/style.css +++ b/src/controls/list-item/style.css @@ -19,11 +19,12 @@ .expander, .expanderPlaceholder { width: 12px; height: 12px; - margin-right: 4px; + padding-right: 4px; + padding-left: 1px; vertical-align: 2px; } .expander { text-align: center; - font-size: 10px; + font-size: .7em; } diff --git a/src/controls/list/index.jsx b/src/controls/list/index.jsx index 0f6a7d7..5bac503 100644 --- a/src/controls/list/index.jsx +++ b/src/controls/list/index.jsx @@ -8,6 +8,8 @@ const ListItemContext = require("../../contexts/list-item"); const generateGridItemStyle = require("../../util/generate-grid-item-style"); // FIXME: Track full collapsing state throughout the tree +// FIXME: Remove left-margin if there are zero collapsible items (as we don't need to render the collapse icons) +// FIXME: Only trigger select when the selected item has actually changed! module.exports = function List({ x, y, children, onPick, onSelect }) { let [ selectedItem, setSelectedItem ] = React.useState([]); @@ -32,7 +34,7 @@ module.exports = function List({ x, y, children, onPick, onSelect }) { return ( -
+
{children}
diff --git a/src/controls/menu-bar/index.jsx b/src/controls/menu-bar/index.jsx index 5d27ebe..a105136 100644 --- a/src/controls/menu-bar/index.jsx +++ b/src/controls/menu-bar/index.jsx @@ -45,7 +45,7 @@ module.exports = function MenuBar({ style, children }) { return ( -
+
{children}
diff --git a/src/controls/menu-divider/index.jsx b/src/controls/menu-divider/index.jsx index ba30d0d..5fe13da 100644 --- a/src/controls/menu-divider/index.jsx +++ b/src/controls/menu-divider/index.jsx @@ -8,5 +8,5 @@ const defaultStyle = require("./style.css"); module.exports = function MenuDivider() { let { withTheme } = useTheme({ control: "menu", defaultStyle }); - return
; + return
; }; diff --git a/src/controls/menu-item/index.jsx b/src/controls/menu-item/index.jsx index 0fa0869..d4eabe2 100644 --- a/src/controls/menu-item/index.jsx +++ b/src/controls/menu-item/index.jsx @@ -65,6 +65,7 @@ module.exports = function MenuItem({ label, title, menu, onClick }) { }; let buttonClasses = withTheme( + "uilibComponent", "item", { item_selected: isSelected }, { item_directPress: (menu == null) } diff --git a/src/controls/menu/index.jsx b/src/controls/menu/index.jsx index f3dbdf1..cb9f6d0 100644 --- a/src/controls/menu/index.jsx +++ b/src/controls/menu/index.jsx @@ -43,7 +43,7 @@ module.exports = function Menu({ children }) { if (parentItemContext.isActive) { return ( -
+
{children}
diff --git a/src/controls/pane-set/index.jsx b/src/controls/pane-set/index.jsx new file mode 100644 index 0000000..a570cd5 --- /dev/null +++ b/src/controls/pane-set/index.jsx @@ -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 ( +
+ {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 + } + }; + })} +
+ ); +}; diff --git a/src/controls/pane-set/style.css b/src/controls/pane-set/style.css new file mode 100644 index 0000000..44456ae --- /dev/null +++ b/src/controls/pane-set/style.css @@ -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%; + } +} diff --git a/src/controls/pane/index.jsx b/src/controls/pane/index.jsx new file mode 100644 index 0000000..6b7e86e --- /dev/null +++ b/src/controls/pane/index.jsx @@ -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 ( +
+
+
+ ); +} + +// 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 ; + } + + // FIXME: Do we still need the contentWrapper, without the bounds-measuring logic? + let content = ( +
+ {children} +
+ ); + + return ( +
+ {(resizable === true) + ? + {content} + + : content + } +
+ ); +}; diff --git a/src/controls/pane/style.css b/src/controls/pane/style.css new file mode 100644 index 0000000..0b21f87 --- /dev/null +++ b/src/controls/pane/style.css @@ -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); + } +} diff --git a/src/controls/progress-bar/index.jsx b/src/controls/progress-bar/index.jsx index 713d5fa..3150414 100644 --- a/src/controls/progress-bar/index.jsx +++ b/src/controls/progress-bar/index.jsx @@ -11,7 +11,7 @@ module.exports = function ProgressBar({ x, y, progress, color }) { let { withTheme } = useTheme({ control: "progressBar", defaultStyle }); return ( -
+
); diff --git a/src/controls/progress-button/index.jsx b/src/controls/progress-button/index.jsx index d2303ad..f08a375 100644 --- a/src/controls/progress-button/index.jsx +++ b/src/controls/progress-button/index.jsx @@ -9,7 +9,7 @@ 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 + // FIXME: progressColor in theme, same for custom button style / proportions -- maybe make `type` a thing that takes an array, for multiple type classes? to deal with composite controls let { withTheme } = useTheme({ control: "progressButton", defaultStyle }); @@ -18,7 +18,7 @@ module.exports = function ProgressButton({ x, y, horizontal, vertical, type, ico : "horizontal"; return ( -
+
diff --git a/src/controls/ribbon-box/index.jsx b/src/controls/ribbon-box/index.jsx index ae43711..ea51265 100644 --- a/src/controls/ribbon-box/index.jsx +++ b/src/controls/ribbon-box/index.jsx @@ -11,7 +11,7 @@ module.exports = function RibbonBox({ label, children, width, height }) { // FIXME: How to handle width/height here? Is the current approach correct? Or should we let the user override this through a class? return ( -
+
{children}
diff --git a/src/controls/ribbon-box/style.css b/src/controls/ribbon-box/style.css index c8a86fd..2072f71 100644 --- a/src/controls/ribbon-box/style.css +++ b/src/controls/ribbon-box/style.css @@ -3,6 +3,7 @@ grid-template-rows: 1fr auto; margin: 4px 0px; padding: 0px 8px; + background-color: transparent; &:first-child { border-left: none; diff --git a/src/controls/ribbon/index.jsx b/src/controls/ribbon/index.jsx index ccf1853..1337e32 100644 --- a/src/controls/ribbon/index.jsx +++ b/src/controls/ribbon/index.jsx @@ -10,7 +10,7 @@ module.exports = function Ribbon({ x, y, children }) { let { withTheme } = useTheme({ control: "ribbon", defaultStyle }); return ( -
+
{children}
); diff --git a/src/controls/ribbon/style.css b/src/controls/ribbon/style.css index f2a5924..7338393 100644 --- a/src/controls/ribbon/style.css +++ b/src/controls/ribbon/style.css @@ -4,6 +4,7 @@ :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("../status-indicator/style.css") { statusIndicator: statusIndicator; } :import("../shared.css") { combinedButton: combinedButton; } .ribbon { @@ -44,4 +45,8 @@ .progressButton { display: grid; } + + .statusIndicator { + height: 100%; + } } diff --git a/src/controls/shared.css b/src/controls/shared.css index 4b48996..f49e6c0 100644 --- a/src/controls/shared.css +++ b/src/controls/shared.css @@ -34,3 +34,71 @@ .combinedVertical { /* flex-direction: column; */ } + +:global { + .react-resizable { + position: relative; + } + .react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; + background-repeat: no-repeat; + background-origin: content-box; + box-sizing: border-box; + background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+'); + background-position: bottom right; + padding: 0 3px 3px 0; + } + .react-resizable-handle-sw { + bottom: 0; + left: 0; + cursor: sw-resize; + transform: rotate(90deg); + } + .react-resizable-handle-se { + bottom: 0; + right: 0; + cursor: se-resize; + } + .react-resizable-handle-nw { + top: 0; + left: 0; + cursor: nw-resize; + transform: rotate(180deg); + } + .react-resizable-handle-ne { + top: 0; + right: 0; + cursor: ne-resize; + transform: rotate(270deg); + } + .react-resizable-handle-w, + .react-resizable-handle-e { + top: 50%; + margin-top: -10px; + cursor: ew-resize; + } + .react-resizable-handle-w { + left: 0; + transform: rotate(135deg); + } + .react-resizable-handle-e { + right: 0; + transform: rotate(315deg); + } + .react-resizable-handle-n, + .react-resizable-handle-s { + left: 50%; + margin-left: -10px; + cursor: ns-resize; + } + .react-resizable-handle-n { + top: 0; + transform: rotate(225deg); + } + .react-resizable-handle-s { + bottom: 0; + transform: rotate(45deg); + } +} diff --git a/src/controls/status-indicator/index.jsx b/src/controls/status-indicator/index.jsx new file mode 100644 index 0000000..abcb4ac --- /dev/null +++ b/src/controls/status-indicator/index.jsx @@ -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 ( +
+
+ {children} +
+
+ ); +}; diff --git a/src/controls/status-indicator/style.css b/src/controls/status-indicator/style.css new file mode 100644 index 0000000..1591caf --- /dev/null +++ b/src/controls/status-indicator/style.css @@ -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; +} diff --git a/src/controls/text/index.jsx b/src/controls/text/index.jsx index f72f92e..557294e 100644 --- a/src/controls/text/index.jsx +++ b/src/controls/text/index.jsx @@ -14,7 +14,7 @@ module.exports = function Text({ x, y, align, children }) { : undefined; return ( -
+
{children}
); diff --git a/src/controls/text/style.css b/src/controls/text/style.css index ad89323..0826a3e 100644 --- a/src/controls/text/style.css +++ b/src/controls/text/style.css @@ -1,5 +1,7 @@ .text { + /* FIXME: Should be moved to ribbon-only, but this is probably not possible with ICSS */ composes: centerContent from "../shared.css"; + background-color: transparent; } .align-left { diff --git a/src/index.js b/src/index.js index 189ef18..8401afd 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ module.exports = { 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"), + StatusIndicator: require("./controls/status-indicator/index.jsx"), // /* Pane layout */ // PaneLayout: require("./controls/pane-layout/layout.jsx"), @@ -40,11 +40,18 @@ module.exports = { // RibbonStatusIndicator: require("./controls/ribbon/status-indicator.jsx"), // RibbonText: require("./controls/ribbon/text.jsx") + Pane: require("./controls/pane"), + PaneSet: require("./controls/pane-set"), + SetTheme: require("./util/themeable/set-theme"), themes: { dark: { getIcon: (name) => `/icons/${name}.svg`, css: require("./themes/dark.css") + }, + light: { + getIcon: (name) => `/icons/${name}.svg`, + css: require("./themes/light.css") } } }; diff --git a/src/themes/dark.css b/src/themes/dark.css index 754d7f2..82074f6 100644 --- a/src/themes/dark.css +++ b/src/themes/dark.css @@ -1,4 +1,5 @@ $darkGray: rgb(34, 34, 34); +$lessDarkGray: rgb(44, 44, 44); $hoverColor: rgba(113, 113, 113, 0.26); $activeColor: $darkGray; @@ -25,13 +26,23 @@ body { inset -1px -1px rgba(135, 131, 131, 0.3); } -.bar { - composes: edgeRaise; - background-color: rgb(60, 62, 66); +.uilibThemedElement { + color: white; } +/* Default to a black background for any newly-defined components that don't have their own styles yet, for minimum readability */ .uilibComponent { - color: white; + background-color: black; +} + +/* ... but don't apply this to known container-type components which don't have their own styling */ +.icon_icon, .grid_grid, .ribbonBox_box { + background-color: transparent; +} + +.bar { + composes: edgeRaise; + background-color: rgb(60, 62, 66); } .list_list { @@ -89,7 +100,7 @@ body { .button_button { background-color: #38383c; - // background-color: magenta; + /* background-color: magenta; */ border: none; &:hover { @@ -126,6 +137,12 @@ body { background-color: rgb(54, 135, 18); } +.pane_handle { + background-color: $darkGray; + composes: edgeRaise; +} + .ribbon_ribbon { /* ... */ } + diff --git a/src/themes/light.css b/src/themes/light.css new file mode 100644 index 0000000..dcb5fc2 --- /dev/null +++ b/src/themes/light.css @@ -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 { + /* ... */ +} diff --git a/src/util/children-with-props.js b/src/util/children-with-props.js new file mode 100644 index 0000000..1927af5 --- /dev/null +++ b/src/util/children-with-props.js @@ -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; + } + }); +}; diff --git a/src/util/generate-grid-template-string.js b/src/util/generate-grid-template-string.js new file mode 100644 index 0000000..dc0ba64 --- /dev/null +++ b/src/util/generate-grid-template-string.js @@ -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(" "); +}; diff --git a/src/util/sum.js b/src/util/sum.js new file mode 100644 index 0000000..8873e11 --- /dev/null +++ b/src/util/sum.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function sum(numbers) { + return numbers.reduce((total, number) => total + number, 0); +}; diff --git a/src/util/themeable/index.jsx b/src/util/themeable/index.jsx index e8a7435..8e2ef43 100644 --- a/src/util/themeable/index.jsx +++ b/src/util/themeable/index.jsx @@ -20,6 +20,8 @@ function mapDefaultName(defaultStyle, className) { module.exports = function useTheme({ control, defaultStyle }) { let theme = React.useContext(ThemeContext); + console.log({ css: theme.css }); + let rulePrefix = (control != null) ? `${control}_` : ""; @@ -37,9 +39,16 @@ module.exports = function useTheme({ control, defaultStyle }) { (theme != null) ? mapThemeName(theme.css, className, rulePrefix) : null, - mapDefaultName(globalStyle, "uilibComponent"), + mapDefaultName(globalStyle, "uilibThemedElement"), (theme != null) - ? mapThemeName(theme.css, "uilibComponent", "") + ? mapThemeName(theme.css, "uilibThemedElement", "") + : null, + // Special case: unprefixed control-independent class names -- FIXME: This check does not work for conditional class names! + (className === "uilibComponent") + ? mapDefaultName(globalStyle, className) + : null, + (theme != null && className === "uilibComponent") + ? mapThemeName(theme.css, className, "") : null, ]; }), diff --git a/src/util/use-disable-selection.js b/src/util/use-disable-selection.js new file mode 100644 index 0000000..ac270e7 --- /dev/null +++ b/src/util/use-disable-selection.js @@ -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; + } + } + }; +}; diff --git a/src/util/use-indexed-state.js b/src/util/use-indexed-state.js new file mode 100644 index 0000000..742ff77 --- /dev/null +++ b/src/util/use-indexed-state.js @@ -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 + ]; +}; diff --git a/yarn.lock b/yarn.lock index dbd55db..96c981c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1113,7 +1113,12 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -classnames@^2.2.6: +clamp@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634" + integrity sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ= + +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -1841,7 +1846,7 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2115,7 +2120,7 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.x, prop-types@^15.6.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -2129,11 +2134,27 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +react-draggable@^4.0.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3" + integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + react-is@^16.8.1: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-resizable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-2.0.0.tgz#e254908acd949e52015ecf56e9788dc73b8b510d" + integrity sha512-oNVMKk+IQNW+nxcCB1W1uShZLJhIh3TDSW8NAbfck6N2jsiUTQ/V5ozBVFEHlRsxgxkf2A22rZJnXLRCBF14OA== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-use-measure@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.0.1.tgz#4f23f94c832cd4512da55acb300d1915dcbf3ae8" @@ -2141,10 +2162,6 @@ react-use-measure@^2.0.1: dependencies: debounce "^1.2.0" -"react@link:../site-builder/node_modules/react": - version "0.0.0" - uid "" - readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -2471,6 +2488,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +timm@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" + integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"