WIP
commit
63fee8ca26
@ -0,0 +1 @@
|
||||
node_modules
|
@ -0,0 +1,7 @@
|
||||
# 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?)
|
||||
|
||||
# Name ideas
|
||||
|
||||
- SnapUI (snap-ui)
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "ui-lib",
|
||||
"description": "A highly-responsive library of UI controls, as React components",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.js",
|
||||
"repository": "http://git.cryto.net/joepie91/ui-lib.git",
|
||||
"author": "Sven Slootweg <admin@cryto.net>",
|
||||
"license": "WTFPL OR CC0-1.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@joepie91/eslint-config": "^1.1.0",
|
||||
"babelify": "^10.0.0",
|
||||
"eslint": "^6.7.0",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"eslint-plugin-react-hooks": "^2.3.0",
|
||||
"icssify": "^1.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-hook/debounce": "^3.0.0",
|
||||
"as-expression": "^1.0.0",
|
||||
"assure-array": "^1.0.0",
|
||||
"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-use-measure": "^2.0.1",
|
||||
"syncpipe": "^1.0.0"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
[
|
||||
"babelify",
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugin": [
|
||||
"icssify"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = React.createContext();
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = React.createContext();
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = React.createContext([]);
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = React.createContext();
|
@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const Icon = require("../icon");
|
||||
|
||||
module.exports = function Button({ type, onClick, label, icon }) {
|
||||
// TODO: Validate type?
|
||||
// TODO: Content override instead of label/icon?
|
||||
let { withTheme } = useTheme({ control: "button", defaultStyle });
|
||||
|
||||
return (
|
||||
<button className={withTheme("button", `style-${type}`)} onClick={onClick}>
|
||||
<span className="buttonContents">
|
||||
{(icon != null)
|
||||
? <Icon icon={icon} />
|
||||
: null
|
||||
}
|
||||
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
/* This file contains dummy classes that are needed to work around the lack of support for circular imports (eg. for recursively nested controls) */
|
||||
|
||||
.menu {}
|
@ -0,0 +1,5 @@
|
||||
/* 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 {
|
||||
font-family: sans-serif;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
|
||||
module.exports = function Icon({ icon }) {
|
||||
// FIXME: Figure out a better way to test stuff than symlinking ui-lib react into site-builder react...
|
||||
let { withTheme } = useTheme({ control: "icon", defaultStyle });
|
||||
|
||||
// FIXME: Don't hardcode icon base path, get it from theme
|
||||
return (
|
||||
<img className={withTheme("icon")} src={`/images/icons/${icon}.svg`} />
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
.icon {
|
||||
/* placeholder */
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const asExpression = require("as-expression");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const ListItemContext = require("../../contexts/list-item");
|
||||
const comparePath = require("../../util/compare-path");
|
||||
const useClickTimer = require("../../util/use-click-timer");
|
||||
|
||||
function Expander({ expanded, onClick }) {
|
||||
let { withTheme } = useTheme({ control: "list", defaultStyle });
|
||||
|
||||
return (
|
||||
<div className={withTheme("expander")} onClick={onClick}>
|
||||
{(expanded === true)
|
||||
? "▼"
|
||||
: "▶"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpanderPlaceholder() {
|
||||
let { withTheme } = useTheme({ control: "list", defaultStyle });
|
||||
|
||||
return <div className={withTheme("expanderPlaceholder")} />;
|
||||
}
|
||||
|
||||
module.exports = function ListItem({ label, id, children }) {
|
||||
let { onClick, onDoubleClick, path, selectedPath } = React.useContext(ListItemContext);
|
||||
let [ expanded, setExpanded ] = React.useState(true);
|
||||
let { withTheme } = useTheme({ control: "list", defaultStyle });
|
||||
let doubleClickTimer = useClickTimer(500);
|
||||
|
||||
let depth = path.length;
|
||||
let indent = depth * 11; // FIXME: Figure out a way to do this with CSS instead, eg. via value exports?
|
||||
let ownPath = path.concat([ id ]);
|
||||
let isSelected = comparePath(selectedPath, ownPath);
|
||||
|
||||
function handleClick() {
|
||||
if (doubleClickTimer.click()) {
|
||||
if (onDoubleClick != null) {
|
||||
onDoubleClick(ownPath);
|
||||
} else if (children != null) {
|
||||
// TODO: Check whether we really only want to do this when no onDoubleClick handler is specified, ie. only as a default behaviour
|
||||
toggleExpanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown() {
|
||||
if (doubleClickTimer.mouseDown()) {
|
||||
onClick(ownPath);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
setExpanded((expanded) => !expanded);
|
||||
}
|
||||
|
||||
let style = {
|
||||
paddingLeft: `${indent}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* FIXME: item_selected capitalization */}
|
||||
<div className={withTheme(["item", { item_selected: isSelected }])} style={style} onMouseDown={handleMouseDown} onClick={handleClick}>
|
||||
{(children != null)
|
||||
? <Expander expanded={expanded} onClick={toggleExpanded} />
|
||||
: <ExpanderPlaceholder />}
|
||||
<div className={withTheme("label")}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
{(children != null && expanded === true)
|
||||
? asExpression(() => {
|
||||
let itemContext = {
|
||||
path: ownPath,
|
||||
selectedPath: selectedPath,
|
||||
onClick: onClick,
|
||||
onDoubleClick: onDoubleClick
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItemContext.Provider value={itemContext}>
|
||||
{children}
|
||||
</ListItemContext.Provider>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
:import("../icon/style.css") {
|
||||
icon: icon;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.expander, .expanderPlaceholder, .label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.expander, .expanderPlaceholder {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
.expander {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const ListItemContext = require("../../contexts/list-item");
|
||||
|
||||
// FIXME: Track full collapsing state throughout the tree
|
||||
|
||||
module.exports = function List({ children, onPick, onSelect }) {
|
||||
let [ selectedItem, setSelectedItem ] = React.useState([]);
|
||||
let { withTheme } = useTheme({ control: "list", defaultStyle });
|
||||
|
||||
let itemContext = {
|
||||
path: [],
|
||||
selectedPath: selectedItem,
|
||||
onClick: function (path) {
|
||||
if (onSelect != null) {
|
||||
onSelect(path);
|
||||
}
|
||||
|
||||
setSelectedItem(path);
|
||||
},
|
||||
onDoubleClick: function (path) {
|
||||
if (onPick != null) {
|
||||
onPick(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItemContext.Provider value={itemContext}>
|
||||
<div className={withTheme("list")}>
|
||||
{children}
|
||||
</div>
|
||||
</ListItemContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
.list {
|
||||
/* @include edge-lower; */
|
||||
overflow-y: auto;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const isEventInsideRef = require("../../util/is-event-inside-ref");
|
||||
const MenuItemContext = require("../../contexts/menu-item");
|
||||
|
||||
module.exports = function MenuBar({ style, children }) {
|
||||
let { withTheme } = useTheme({ control: "menu", defaultStyle });
|
||||
let [ isActive, setIsActive ] = React.useState(false);
|
||||
let [ selectedItem, setSelectedItem ] = React.useState([]);
|
||||
let element = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleGlobalClick(event) {
|
||||
if (!isEventInsideRef(element, event)) {
|
||||
setIsActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleGlobalClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleGlobalClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let itemContext = {
|
||||
menuType: "menuBar",
|
||||
selectedPath: selectedItem,
|
||||
isActive: isActive,
|
||||
onMouseEnter: (path) => {
|
||||
setSelectedItem(path);
|
||||
},
|
||||
onClick: (_path, hasMenu, isInMenu) => {
|
||||
// Only activate the menu bar if this *isn't* a direct-action button directly on the menubar
|
||||
if (hasMenu || isInMenu) {
|
||||
// NOTE: This is a toggle that sets isActive for the *whole* menu bar rather than an individual item, because pressing *any* button in the menu will trigger menu-display mode; after that, merely hovering over other buttons is enough to switch what menu is displayed.
|
||||
setIsActive((isActive) => !isActive);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItemContext.Provider value={itemContext}>
|
||||
<div className={withTheme("menuBar")} style={style} ref={element}>
|
||||
{children}
|
||||
</div>
|
||||
</MenuItemContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
:import("../menu-item/style.css") {
|
||||
item: item;
|
||||
submenu: submenu;
|
||||
submenuArrow: submenuArrow;
|
||||
}
|
||||
|
||||
.menuBar {
|
||||
composes: notSelectable from "../shared.css";
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.menuBar > .item {
|
||||
padding: 4px 7px 5px 7px;
|
||||
}
|
||||
|
||||
.menuBar > .item > .submenu > .submenuArrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menuBar .icon {
|
||||
color: red; /* ??? */
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
const React = require("react");
|
||||
const useMeasure = require("react-use-measure");
|
||||
const matchValue = require("match-value");
|
||||
const insecureNanoid = require("nanoid/non-secure").nanoid;
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const useGuaranteedMemo = require("../../util/use-guaranteed-memo");
|
||||
const isEventInsideRef = require("../../util/is-event-inside-ref");
|
||||
const comparePath = require("../../util/compare-path");
|
||||
const MenuItemContext = require("../../contexts/menu-item");
|
||||
const MenuPositionContext = require("../../contexts/menu-position");
|
||||
const MenuPathContext = require("../../contexts/menu-path");
|
||||
|
||||
module.exports = function MenuItem({ label, title, menu, onClick }) {
|
||||
let { withTheme } = useTheme({ control: "menu", defaultStyle });
|
||||
let [ boundsRef, bounds ] = useMeasure({ debounce: 10 });
|
||||
let submenuRef = React.useRef();
|
||||
let itemContext = React.useContext(MenuItemContext);
|
||||
let path = React.useContext(MenuPathContext);
|
||||
let id = useGuaranteedMemo(() => insecureNanoid()); // insecure is fine here, these IDs are not interesting to an attacker
|
||||
|
||||
let ownPath = path.concat([ id ]); // FIXME: memoize?
|
||||
let isSelected = itemContext.isActive && comparePath(itemContext.selectedPath, ownPath);
|
||||
|
||||
let submenuPosition = matchValue(itemContext.menuType, {
|
||||
menuBar: {
|
||||
x: bounds.x,
|
||||
y: bounds.y + bounds.height
|
||||
},
|
||||
menu: {
|
||||
x: bounds.x + bounds.width, /* FIXME: Account for border of containing menu? */
|
||||
y: bounds.y
|
||||
}
|
||||
});
|
||||
|
||||
let handlers = {
|
||||
onClick: (event) => {
|
||||
if (!isEventInsideRef(submenuRef, event)) {
|
||||
if (onClick != null) {
|
||||
onClick(); // TODO: Pass through event? Allow cancellation? To deal with advanced cases like checkboxes within menus, if we want that
|
||||
}
|
||||
|
||||
if (itemContext.onClick != null) {
|
||||
let hasMenu = (menu != null);
|
||||
let isInMenu = false; // Let the menu intercept and change this
|
||||
|
||||
itemContext.onClick(ownPath, hasMenu, isInMenu);
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseEnter: (_event) => {
|
||||
// FIXME: Only outside of submenu?
|
||||
if (itemContext.onMouseEnter != null) {
|
||||
itemContext.onMouseEnter(ownPath);
|
||||
}
|
||||
},
|
||||
onMouseLeave: (_event) => {
|
||||
// FIXME: Only outside of submenu?
|
||||
if (itemContext.onMouseLeave != null) {
|
||||
itemContext.onMouseLeave(ownPath);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let buttonClasses = withTheme([
|
||||
"item",
|
||||
{ item_selected: isSelected },
|
||||
{ item_directPress: (menu == null) }
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenuPathContext.Provider value={ownPath}>
|
||||
<MenuPositionContext.Provider value={submenuPosition}>
|
||||
<div ref={boundsRef} title={title} className={buttonClasses} {...handlers}>
|
||||
{label}
|
||||
|
||||
{(menu != null)
|
||||
? <span className={withTheme("submenu")} ref={submenuRef}>
|
||||
<i className={withTheme("submenuArrow")}>▶</i>
|
||||
{(isSelected)
|
||||
? menu
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</MenuPositionContext.Provider>
|
||||
</MenuPathContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
:import("../dummy-classes.css") {
|
||||
menu: menu;
|
||||
}
|
||||
|
||||
.item .menu {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
/* Placeholder */
|
||||
}
|
||||
|
||||
.submenuArrow {
|
||||
float: right;
|
||||
font-style: normal;
|
||||
margin-left: .8em;
|
||||
font-size: .8em;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const { useDebounce } = require("@react-hook/debounce");
|
||||
|
||||
const MenuItemContext = require("../../contexts/menu-item");
|
||||
const MenuPositionContext = require("../../contexts/menu-position");
|
||||
const useTheme = require("../../util/themeable");
|
||||
const defaultStyle = require("./style.css");
|
||||
|
||||
// FIXME: Use flexbox to deal with overflow of long menus
|
||||
|
||||
module.exports = function Menu({ children }) {
|
||||
let { withTheme } = useTheme({ control: "menu", defaultStyle });
|
||||
let parentItemContext = React.useContext(MenuItemContext);
|
||||
let menuPosition = React.useContext(MenuPositionContext);
|
||||
// FIXME: We don't want a debounce, but a delay timer for submenu (dis)appearance? Separate from hover effect for the menu item itself. Debounce is insufficient because if one keeps moving the submenu never disappears
|
||||
let [ selectedItem, setSelectedItem ] = useDebounce([], 150);
|
||||
|
||||
let itemContext = {
|
||||
menuType: "menu",
|
||||
selectedPath: selectedItem,
|
||||
isActive: parentItemContext.isActive,
|
||||
onMouseEnter: (path) => {
|
||||
setSelectedItem(path);
|
||||
},
|
||||
onClick: (path, hasMenu, _isInMenu) => {
|
||||
// Items with submenus should not be considered clickable
|
||||
if (!hasMenu) {
|
||||
parentItemContext.onClick(path, false, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: Is there ever a case where this *can* validly be null?
|
||||
let style = (menuPosition != null)
|
||||
? {
|
||||
left: menuPosition.x,
|
||||
top: menuPosition.y
|
||||
}
|
||||
: null;
|
||||
|
||||
if (parentItemContext.isActive) {
|
||||
return (
|
||||
<MenuItemContext.Provider value={itemContext}>
|
||||
<div className={withTheme("menu")} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
</MenuItemContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
:import("../menu-item/style.css") {
|
||||
item: item;
|
||||
}
|
||||
|
||||
:import("../dummy-classes.css") {
|
||||
menu_dummy: menu;
|
||||
}
|
||||
|
||||
.menu {
|
||||
/* TODO: Is there also a compose-multiple syntax for remote compose? */
|
||||
/* HACK: We cannot compose directly inside of an imported class (is this an ICSS bug?), so instead we create a new class and compose the dummy class into it, to tie the two together */
|
||||
composes: menu_dummy;
|
||||
composes: notSelectable from "../shared.css";
|
||||
min-width: 150px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.menu > .item {
|
||||
padding: 4px 9px;
|
||||
}
|
||||
|
||||
.menu hr {
|
||||
color: none;
|
||||
border: none;
|
||||
margin: 4px 4px;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const defaultStyle = require("./style.css");
|
||||
const useTheme = require("../../util/themeable");
|
||||
|
||||
module.exports = function Ribbon({ children }) {
|
||||
let { withTheme } = useTheme({ control: "ribbon", defaultStyle });
|
||||
|
||||
return (
|
||||
<div className={withTheme("ribbon")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
.ribbon {
|
||||
composes: notSelectable from "../shared.css";
|
||||
display: flex;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
.notSelectable {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-khtml-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.centerContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
/* Multi-purpose controls */
|
||||
Icon: require("./controls/icon/index.jsx"),
|
||||
// Text: require("./controls/text.jsx"),
|
||||
|
||||
// /* Pane layout */
|
||||
// PaneLayout: require("./controls/pane-layout/layout.jsx"),
|
||||
// PaneRow: require("./controls/pane-layout/row.jsx"),
|
||||
// PaneTabSet: require("./controls/pane-layout/tabset.jsx"),
|
||||
// PaneTab: require("./controls/pane-layout/tab.jsx"),
|
||||
|
||||
// /* Menu */
|
||||
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"),
|
||||
|
||||
// /* 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"),
|
||||
// // RibbonItem: require("./ribbon/item.jsx"), // Used internally, but not exported (yet)
|
||||
// RibbonButton: require("./controls/ribbon/button.jsx"),
|
||||
// RibbonButtonSet: require("./controls/ribbon/button-set.jsx"),
|
||||
// RibbonListBox: require("./controls/ribbon/list-box.jsx"),
|
||||
// RibbonListButton: require("./controls/ribbon/list-button.jsx"),
|
||||
// RibbonProgressBar: require("./controls/ribbon/progress-bar.jsx"),
|
||||
// RibbonProgressButton: require("./controls/ribbon/progress-button.jsx"),
|
||||
// RibbonStatusIndicator: require("./controls/ribbon/status-indicator.jsx"),
|
||||
// RibbonText: require("./controls/ribbon/text.jsx")
|
||||
|
||||
SetTheme: require("./util/themeable/set-theme"),
|
||||
themes: {
|
||||
dark: {
|
||||
getIcon: (name) => `/icons/${name}.svg`,
|
||||
css: require("./themes/dark.css")
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
$darkGray: rgb(34, 34, 34);
|
||||
$hoverColor: rgba(113, 113, 113, 0.26);
|
||||
$activeColor: $darkGray;
|
||||
|
||||
.edgeRaise {
|
||||
box-shadow: inset -1px -1px rgba(30, 27, 27, 0.7),
|
||||
inset 1px 1px rgba(135, 131, 131, 0.3);
|
||||
}
|
||||
|
||||
.edgeLower {
|
||||
box-shadow: inset 1px 1px rgba(30, 27, 27, 0.7),
|
||||
inset -1px -1px rgba(135, 131, 131, 0.3);
|
||||
}
|
||||
|
||||
.bar {
|
||||
composes: edgeRaise;
|
||||
background-color: rgb(60, 62, 66);
|
||||
}
|
||||
|
||||
.list_list {
|
||||
/* color: red; */
|
||||
}
|
||||
|
||||
.list_item {
|
||||
color: white;
|
||||
|
||||
&: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 {
|
||||
composes: bar;
|
||||
}
|
||||
|
||||
.menu_menuBar > .menu_item {
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
|
||||
&.menu_item_selected, &menu_item_directPress {
|
||||
background-color: $activeColor;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_menu {
|
||||
background-color: $darkGray;
|
||||
box-shadow: 1px 1px 2px rgb(54, 54, 54);
|
||||
|
||||
& > .menu_item:hover {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 1px solid rgb(78, 78, 78);
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon_ribbon {
|
||||
composes: bar;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function createClickTimer({ timeout }) {
|
||||
let timer;
|
||||
|
||||
// NOTE: We have separate handling for mouseDown and click events, so that even if the user double-clicks, the response to initial click feels instant.
|
||||
return {
|
||||
mouseDown: function () {
|
||||
// returns: is first click
|
||||
return (timer == null);
|
||||
},
|
||||
click: function () {
|
||||
// returns: is double click
|
||||
if (timer == null) {
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
}, timeout);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
timer = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
isRunning: function () {
|
||||
return (timer != null);
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function comparePath(a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
} else {
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
const assureArray = require("assure-array");
|
||||
const syncpipe = require("syncpipe");
|
||||
const flatten = require("flatten");
|
||||
|
||||
module.exports = function extendClassnames(classes, mapper) {
|
||||
return assureArray(classes).map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return mapper(item);
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map((subItem) => {
|
||||
return mapper(subItem);
|
||||
});
|
||||
} else {
|
||||
// { className: condition } mapping
|
||||
return syncpipe(item, [
|
||||
(_) => Object.entries(_),
|
||||
(_) => _.map(([ key, value ]) => syncpipe(key, [
|
||||
(_) => mapper(_),
|
||||
(_) => assureArray(_),
|
||||
(_) => _.filter((className) => className != null),
|
||||
(_) => _.map((className) => [ className, value ])
|
||||
])),
|
||||
(_) => flatten(_, 1),
|
||||
(_) => Object.fromEntries(_),
|
||||
]);
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function isEventInsideRef(ref, event) {
|
||||
return (ref.current != null && ref.current.contains(event.target));
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = React.createContext();
|
@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const classnames = require("classnames");
|
||||
const defaultValue = require("default-value");
|
||||
|
||||
const extendClassnames = require("../extend-classnames");
|
||||
const ThemeContext = require("./context");
|
||||
const globalStyle = require("../../controls/glo-bal-style.css");
|
||||
|
||||
function mapThemeName(themeCSS, className, rulePrefix) {
|
||||
return themeCSS[`${rulePrefix}${className}`];
|
||||
}
|
||||
|
||||
function mapDefaultName(defaultStyle, className) {
|
||||
return defaultStyle[className];
|
||||
}
|
||||
|
||||
module.exports = function useTheme({ control, defaultStyle }) {
|
||||
let theme = React.useContext(ThemeContext);
|
||||
|
||||
let rulePrefix = (control != null)
|
||||
? `${control}_`
|
||||
: "";
|
||||
|
||||
function withTheme(classes) {
|
||||
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 { theme, withTheme };
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
const ThemeContext = require("./context");
|
||||
|
||||
module.exports = function SetTheme({ theme, children }) {
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const useLazyRef = require("./use-lazy-ref");
|
||||
const createClickTimer = require("./click-timer");
|
||||
|
||||
module.exports = function useClickTimer(timeout) {
|
||||
let ref = useLazyRef(() => createClickTimer({ timeout: timeout }));
|
||||
return ref.current;
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
// Unlike React.useMemo, this one is guaranteed to never drop its value. Useful for things like stable ID generation.
|
||||
module.exports = function useGuaranteedMemo(initializer, dependencies = []) {
|
||||
let [ value, setValue ] = React.useState();
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(initializer);
|
||||
}, dependencies);
|
||||
|
||||
return value;
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
module.exports = function useLazyRef(initializer) {
|
||||
let ref = React.useRef({ initialized: false, value: undefined });
|
||||
|
||||
if (ref.current.initialized === false) {
|
||||
ref.current.value = {
|
||||
// We mirror the standard useRef structure here
|
||||
current: initializer()
|
||||
};
|
||||
|
||||
ref.current.initializer = true;
|
||||
}
|
||||
|
||||
return ref.current.value;
|
||||
};
|
Loading…
Reference in New Issue