(isDark ? `4px solid ${t.colors.toggleIcon}` : `none`),
- backgroundColor: isDark ? `toggleIcon` : `transparent`,
- transform: isDark ? `scale(0.55)` : `scale(1)`,
- transition: `all 0.45s ease`,
- overflow: isDark ? `visible` : `hidden`,
- boxShadow: t =>
- isDark ? `none` : `inset 8px -8px 0px 0px ${t.colors.toggleIcon}`,
- '&:before': {
- content: `""`,
- position: `absolute`,
- right: `-9px`,
- top: `-9px`,
- height: `24px`,
- width: `24px`,
- border: t => (isDark ? `2px solid ${t.colors.toggleIcon}` : `none`),
- borderRadius: `50%`,
- transform: isDark ? `translate(14px, -14px)` : `translate(0, 0)`,
- opacity: isDark ? 0 : 1,
- transition: `transform 0.45s ease`,
- },
- '&:after': {
- content: `""`,
- width: `8px`,
- height: `8px`,
- borderRadius: `50%`,
- margin: `-4px 0 0 -4px`,
- position: `absolute`,
- top: `50%`,
- left: `50%`,
- boxShadow: t =>
- `0 -23px 0 ${t.colors.toggleIcon}, 0 23px 0 ${t.colors.toggleIcon}, 23px 0 0 ${t.colors.toggleIcon}, -23px 0 0 ${t.colors.toggleIcon}, 15px 15px 0 ${t.colors.toggleIcon}, -15px 15px 0 ${t.colors.toggleIcon}, 15px -15px 0 ${t.colors.toggleIcon}, -15px -15px 0 ${t.colors.toggleIcon}`,
- transform: isDark ? `scale(1)` : `scale(0)`,
- transition: `all 0.35s ease`,
- },
- }}
- />
-
-);
diff --git a/integrations/gatsby-theme-stories/src/components/Header.tsx b/integrations/gatsby-theme-stories/src/components/Header.tsx
index d18256ed4..a5db433c3 100644
--- a/integrations/gatsby-theme-stories/src/components/Header.tsx
+++ b/integrations/gatsby-theme-stories/src/components/Header.tsx
@@ -1,41 +1,43 @@
/** @jsx jsx */
-import { FC } from 'react';
-import { jsx, useColorMode, Heading } from 'theme-ui';
+import { FC, useContext } from 'react';
+import { jsx, Heading } from 'theme-ui';
import { Flex } from '@theme-ui/components';
-import { ColorMode } from './ColorMode';
+import { ColorMode, SidebarContext } from '@component-controls/app-components';
interface HeaderProps {
title?: string;
}
export const Header: FC
= ({ title }) => {
- const [colorMode, setColorMode] = useColorMode();
- const isDark = colorMode === `dark`;
- const toggleColorMode = (e: any) => {
- e.preventDefault();
- setColorMode(isDark ? `light` : `dark`);
- };
+ const { SidebarToggle } = useContext(SidebarContext);
return (
-
-
- {title}
-
-
-
+
- links
-
+
+
+ {title}
+
+
+ links
+
+
+
);
};
diff --git a/integrations/gatsby-theme-stories/src/components/Layout.tsx b/integrations/gatsby-theme-stories/src/components/Layout.tsx
index ecb9a1eac..41acd58fe 100644
--- a/integrations/gatsby-theme-stories/src/components/Layout.tsx
+++ b/integrations/gatsby-theme-stories/src/components/Layout.tsx
@@ -1,8 +1,10 @@
/** @jsx jsx */
import React, { FC } from 'react';
-import { jsx, Container } from 'theme-ui';
+import { jsx, Flex, Container } from 'theme-ui';
import { Global } from '@emotion/core';
import { ThemeProvider } from '@component-controls/components';
+import { SidebarContextProvider } from '@component-controls/app-components';
+
import { PageContainer } from '@component-controls/blocks';
import { Store } from '@component-controls/store';
import { SEO } from './SEO';
@@ -35,21 +37,17 @@ export const Layout: FC = ({
})}
/>
-
-
-
-
-
- {children}
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
);
};
diff --git a/integrations/gatsby-theme-stories/src/components/Sidebar.tsx b/integrations/gatsby-theme-stories/src/components/Sidebar.tsx
index 11934ada8..f05bda3dc 100644
--- a/integrations/gatsby-theme-stories/src/components/Sidebar.tsx
+++ b/integrations/gatsby-theme-stories/src/components/Sidebar.tsx
@@ -1,19 +1,67 @@
/** @jsx jsx */
-import { Sidenav } from '@theme-ui/sidenav';
-import { jsx, useColorMode } from 'theme-ui';
-import { graphql, useStaticQuery } from 'gatsby';
+import { FC, useState, useMemo } from 'react';
+import { jsx, Input, LinkProps } from 'theme-ui';
+import { graphql, useStaticQuery, Link as GatsbyLink } from 'gatsby';
import { Story } from '@component-controls/specification';
+import {
+ Sidebar as AppSidebar,
+ Navmenu,
+ MenuItems,
+ MenuItem,
+} from '@component-controls/app-components';
+
import { useSiteMetadata } from '../hooks/use-site-metadata';
-import { ColorMode } from './ColorMode';
-export const Sidebar = () => {
- const { siteTitle } = useSiteMetadata();
- const [colorMode, setColorMode] = useColorMode();
- const isDark = colorMode === `dark`;
- const toggleColorMode = (e: any) => {
- e.preventDefault();
- setColorMode(isDark ? `light` : `dark`);
+const Link: FC = props => (
+ //@ts-ignore
+
+);
+export interface SidebarProps {
+ storyId?: string;
+}
+
+interface ConsolidateKinds {
+ [kind: string]: Story[];
+}
+
+const createMenuItem = (
+ levels: string[],
+ parent?: MenuItems,
+ item?: MenuItem,
+): MenuItem => {
+ if (levels.length < 1) {
+ return item || {};
+ }
+ const newItem: MenuItem = {
+ id: levels[0],
+ label: levels[0],
};
+ const sibling = parent && parent.find(i => i.id === newItem.id);
+ if (parent && !sibling) {
+ newItem.items = [];
+ parent.push(newItem);
+ }
+ return createMenuItem(
+ levels.slice(1),
+ sibling ? sibling.items : newItem.items,
+ newItem,
+ );
+};
+export const Sidebar: FC = ({ storyId }) => {
+ const { siteTitle } = useSiteMetadata();
const data = useStaticQuery(graphql`
query {
@@ -29,48 +77,46 @@ export const Sidebar = () => {
}
`);
const stories = data.allStory.edges;
+
+ const menuItems = useMemo(() => {
+ const kinds: ConsolidateKinds = stories.reduce(
+ (acc: ConsolidateKinds, { node }: { node: Required }) => {
+ if (acc[node.kind]) {
+ return { ...acc, [node.kind]: [...acc[node.kind], node] };
+ }
+ return { ...acc, [node.kind]: [node] };
+ },
+ {},
+ );
+ const menuItems = Object.keys(kinds).reduce((acc: MenuItems, key) => {
+ const stories = kinds[key];
+ const levels = key.split('/');
+ const kind = createMenuItem(levels, acc);
+ kind.items = stories.map(story => ({
+ id: story.id,
+ label: story.name,
+ to: `/stories/${story.id}`,
+ }));
+ return acc;
+ }, []);
+ return menuItems;
+ }, [stories]);
+
+ const [search, setSearch] = useState(undefined);
return (
-
-
- {siteTitle}
-
-
-
li': { fontWeight: `bold`, a: { px: 0 } },
- 'ul > li > ul > li': { paddingLeft: 3 },
- p: 3,
- pt: 2,
- overflowY: [`auto`, `auto`, `initial`],
- width: `initial`,
- }}
- >
- {stories.map(({ node: story }: { node: Story }) => (
- //@ts-ignore
-
- {story.name}
-
- ))}
-
-
+
+ {siteTitle}
+ setSearch(e.target.value)}
+ />
+
+
);
};
diff --git a/integrations/gatsby-theme-stories/src/gatsby-node.ts b/integrations/gatsby-theme-stories/src/gatsby-node.ts
index 5fce2a105..a0b05906e 100644
--- a/integrations/gatsby-theme-stories/src/gatsby-node.ts
+++ b/integrations/gatsby-theme-stories/src/gatsby-node.ts
@@ -80,7 +80,7 @@ exports.createPages = async ({ graphql, actions }: CreatePagesArgs) => {
path: `stories/${node.id}`,
component: require.resolve(`../src/templates/StoryPage.tsx`),
context: {
- title: node.title,
+ title: node.name,
storyId: node.id,
loadedStore,
},
diff --git a/integrations/gatsby-theme-stories/tsconfig.json b/integrations/gatsby-theme-stories/tsconfig.json
index 13309c708..91f043601 100644
--- a/integrations/gatsby-theme-stories/tsconfig.json
+++ b/integrations/gatsby-theme-stories/tsconfig.json
@@ -10,6 +10,6 @@
"baseUrl": "./",
"typeRoots": ["../../node_modules/@types", "node_modules/@types"]
},
- "include": ["src/**/*"],
+ "include": ["src/**/*", "../../ui/app-components/src/Sidebar/Sidebox.tsx"],
"exclude": ["node_modules/**"]
}
\ No newline at end of file
diff --git a/integrations/storybook/README.md b/integrations/storybook/README.md
index f9ee19a89..0aadc66ee 100644
--- a/integrations/storybook/README.md
+++ b/integrations/storybook/README.md
@@ -661,7 +661,7 @@ _StorySource [source code](https:/github.com/ccontrols/component-controls/tree/m
| Name | Type | Description |
| ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `viewStype` | _ViewStyle_ | initial view mode |
+| `viewStyle` | _ViewStyle_ | initial view mode |
| `id` | _string_ | id of the story optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
| `name` | _string_ | alternatively you can use the name of a story to load from an external file |
| `title` | _string_ | optional section title for the block. |
@@ -712,14 +712,9 @@ _PageContextContainer [source code](https:/github.com/ccontrols/component-contro
### properties
-| Name | Type | Description |
-| ------------ | ----------------------- | -------------------------------------------------------------------------------------- |
-| `storyId` | _string_ | story to display in the page |
-| `dark` | _boolean_ | dark/light theme for the page |
-| `options` | _any_ | global options passed from container those are global parameters as well as decorators |
-| `components` | _MDXProviderComponents_ | components to customize the markdown display. |
-| `theme` | _Theme_ | optional custom theme |
-| `store` | _StoryStore_ | store object |
+| Name | Type | Description |
+| ------- | ------- | ----------- |
+| `theme` | _Theme_ | |
## DocsContainer
@@ -727,14 +722,9 @@ _DocsContainer [source code](https:/github.com/ccontrols/component-controls/tree
### properties
-| Name | Type | Description |
-| ------------ | ----------------------- | -------------------------------------------------------------------------------------- |
-| `storyId` | _string_ | story to display in the page |
-| `dark` | _boolean_ | dark/light theme for the page |
-| `options` | _any_ | global options passed from container those are global parameters as well as decorators |
-| `components` | _MDXProviderComponents_ | components to customize the markdown display. |
-| `theme` | _Theme_ | optional custom theme |
-| `store` | _StoryStore_ | store object |
-| `active` | _boolean_ | |
+| Name | Type | Description |
+| -------- | --------- | ----------- |
+| `theme` | _Theme_ | |
+| `active` | _boolean_ | |
diff --git a/plugins/axe-plugin/README.md b/plugins/axe-plugin/README.md
index ae9c09690..9a24355e4 100644
--- a/plugins/axe-plugin/README.md
+++ b/plugins/axe-plugin/README.md
@@ -12,6 +12,7 @@
- [AxeAllyBlock](#insaxeallyblockins)
- [isSelected](#insisselectedins)
- [isTagSelected](#insistagselectedins)
+ - [overview](#insoverviewins)
# In action
@@ -141,4 +142,8 @@ _isSelected [source code](https:/github.com/ccontrols/component-controls/tree/ma
_isTagSelected [source code](https:/github.com/ccontrols/component-controls/tree/master/plugins/axe-plugin/src/components/RecoilContext.tsx)_
+## overview
+
+_overview [source code](https:/github.com/ccontrols/component-controls/tree/master/plugins/axe-plugin/src/stories/AllyBlock.stories.tsx)_
+
diff --git a/ui/app-components/.babelrc b/ui/app-components/.babelrc
new file mode 100644
index 000000000..c64480144
--- /dev/null
+++ b/ui/app-components/.babelrc
@@ -0,0 +1,13 @@
+{
+ "presets": [
+ "@babel/preset-typescript",
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "node": "current"
+ }
+ }
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/ui/app-components/.eslintignore b/ui/app-components/.eslintignore
new file mode 100644
index 000000000..53c37a166
--- /dev/null
+++ b/ui/app-components/.eslintignore
@@ -0,0 +1 @@
+dist
\ No newline at end of file
diff --git a/ui/app-components/LICENSE.md b/ui/app-components/LICENSE.md
new file mode 100644
index 000000000..a64cf9401
--- /dev/null
+++ b/ui/app-components/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Atanas Stoyanov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/ui/app-components/README.md b/ui/app-components/README.md
new file mode 100644
index 000000000..6ecf55d33
--- /dev/null
+++ b/ui/app-components/README.md
@@ -0,0 +1,97 @@
+# Table of contents
+
+- [Overview](#overview)
+- [List of components](#list-of-components)
+ - [ColorMode](#inscolormodeins)
+ - [Keyboard](#inskeyboardins)
+ - [Navmenu](#insnavmenuins)
+ - [Sidebar](#inssidebarins)
+ - [SidebarContextProvider](#inssidebarcontextproviderins)
+
+# Overview
+
+Application components to create standaline user interface for component-controls.
+
+Third-party libraries used in no particular order:
+
+- [theme-ui](https://theme-ui.com) as the theming and components foundation.
+- [octicons](https://octicons.github.com) for icons used in the components.
+
+# List of components
+
+
+
+
+
+## ColorMode
+
+dark/light mode toggle for theme-ui themes
+
+_ColorMode [source code](https:/github.com/ccontrols/component-controls/tree/master/ui/app-components/src/ColorMode/ColorMode.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------- | --------------------- | --------------------------------------------------- |
+| `label` | _string_ | optional label to be displayed alongside the toggle |
+| `ref` | _Ref<ReactSwitch>_ | obtain a ref target |
+
+## Keyboard
+
+Componet to monitor keystrokes. Can attach to child, document or window.
+
+_Keyboard [source code](https:/github.com/ccontrols/component-controls/tree/master/ui/app-components/src/Keyboard/Keyboard.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
+| `keys*` | _number\[]_ | array of the keys to be trapped |
+| `target` | _"children" \| "document" \| "window"_ | to where to attach the event handler |
+| `onKeyDown*` | _KeyboardCallback_ | callbal on key down |
+| `children` | _string \| number \| boolean \| {} \| ReactElement<any, string \| ((props: any) => ReactElement<any, string \| ... \| (new (props: any) => Component<any, any, any>)>) \| (new (props: any) => Component<...>)> \| ReactNodeArray \| ReactPortal \| ReactElement<...>_ | child element to the key event handler will be attached to if target = 'children' |
+
+## Navmenu
+
+Hierarchical collapsible menu
+
+_Navmenu [source code](https:/github.com/ccontrols/component-controls/tree/master/ui/app-components/src/Navmenu/Navmenu.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------- | -------------------------------------------------------------- |
+| `items*` | _MenuItems_ | Array of menu items |
+| `activeItem` | _{ id?: string; label?: string; }_ | Initially active menu item |
+| `buttonClass` | _any_ | Custom class to use for the button instead of Button |
+| `expandAll` | _boolean_ | If specified, will expand all items with chidren |
+| `onSelect` | _(item?: MenuItem) => void_ | Function that will be called when the user selects a menu item |
+| `search` | _string_ | If specified, will filter the items by the search terms |
+
+## Sidebar
+
+Collapsible side bar component
+
+_Sidebar [source code](https:/github.com/ccontrols/component-controls/tree/master/ui/app-components/src/Sidebar/Sidebar.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
+| `title` | _any_ | Title string or any react node |
+| `width` | _number_ | The width of the side bar in pixels |
+| `collapsible` | _boolean_ | Whether the sidebar can be collapsed |
+| `children` | _any_ | children content elements to be displayed in Sidebar |
+| `animate` | _Pick<CollapsibleProps, "slot" \| "style" \| "title" \| "defaultChecked" \| "defaultValue" \| "suppressContentEditableWarning" \| "suppressHydrationWarning" \| "accessKey" \| ... 254 more ... \| "easing">_ | collapsible animate height props |
+
+## SidebarContextProvider
+
+_SidebarContextProvider [source code](https:/github.com/ccontrols/component-controls/tree/master/ui/app-components/src/Sidebar/SidebarContext.tsx)_
+
+### properties
+
+| Name | Type | Description |
+| ------------- | --------- | ----------- |
+| `collapsible` | _boolean_ | |
+
+
diff --git a/ui/app-components/package.json b/ui/app-components/package.json
new file mode 100644
index 000000000..1257429b0
--- /dev/null
+++ b/ui/app-components/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@component-controls/app-components",
+ "version": "1.2.0",
+ "description": "Standalone application components.",
+ "main": "dist/index.js",
+ "module": "dist/index.esm.js",
+ "typings": "dist/index.d.ts",
+ "files": [
+ "dist/",
+ "package.json",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "yarn cross-env NODE_ENV=production rollup -c",
+ "dev": "yarn cross-env NODE_ENV=development rollup -cw",
+ "docs": "ts-node -O '{\"module\": \"commonjs\"}' ../../scripts/docs.ts",
+ "fix": "yarn lint --fix",
+ "lint": "yarn eslint . --ext mdx,ts,tsx",
+ "prepare": "yarn build",
+ "test": "yarn jest --passWithNoTests"
+ },
+ "homepage": "https://github.com/ccontrols/component-controls",
+ "bugs": {
+ "url": "https://github.com/ccontrols/component-controls/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ccontrols/component-controls.git",
+ "directory": "ui/app-components"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@component-controls/components": "^1.2.0",
+ "@primer/octicons-react": "^9.6.0",
+ "react": "^16.8.3",
+ "react-dom": "^16.8.3",
+ "theme-ui": "^0.3.1",
+ "@theme-ui/match-media": "^0.3.1"
+ },
+ "devDependencies": {
+ "@types/jest": "^25.1.2",
+ "@types/theme-ui": "^0.3.0",
+ "cross-env": "^5.2.1",
+ "eslint": "^6.5.1",
+ "jest": "^24.9.0"
+ },
+ "peerDependencies": {
+ "@primer/octicons-react": "*",
+ "react": "*",
+ "react-dom": "*",
+ "theme-ui": "*"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "authors": [
+ "Atanas Stoyanov"
+ ],
+ "gitHead": "c5145d66c6b8a355839e53c3bca97fd361ce9377"
+}
diff --git a/ui/app-components/rollup.config.js b/ui/app-components/rollup.config.js
new file mode 100644
index 000000000..5bf5a8b41
--- /dev/null
+++ b/ui/app-components/rollup.config.js
@@ -0,0 +1,5 @@
+import { config } from '../../rollup-config';
+
+export default config({
+ input: './src/index.ts',
+});
diff --git a/ui/app-components/src/ColorMode/ColorMode.stories.tsx b/ui/app-components/src/ColorMode/ColorMode.stories.tsx
new file mode 100644
index 000000000..d386ba64c
--- /dev/null
+++ b/ui/app-components/src/ColorMode/ColorMode.stories.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { ThemeProvider } from '@component-controls/components';
+import { ColorMode } from '.';
+
+export default {
+ title: 'App components/ColorMode',
+ component: ColorMode,
+};
+
+export const overview = () => (
+
+
+
+);
diff --git a/ui/app-components/src/ColorMode/ColorMode.tsx b/ui/app-components/src/ColorMode/ColorMode.tsx
new file mode 100644
index 000000000..591057827
--- /dev/null
+++ b/ui/app-components/src/ColorMode/ColorMode.tsx
@@ -0,0 +1,73 @@
+/** @jsx jsx */
+import { jsx, useColorMode, Flex } from 'theme-ui';
+import { FC } from 'react';
+import Octicon, {
+ PrimitiveDot,
+ PrimitiveDotStroke,
+} from '@primer/octicons-react';
+import { Toggle, ToggleProps } from '@component-controls/components';
+
+/**
+ * dark/light mode toggle for theme-ui themes
+ */
+export const ColorMode: FC = props => {
+ const [colorMode, setColorMode] = useColorMode();
+ const isDark = colorMode === `dark`;
+ const toggleColorMode = (checked: boolean) => {
+ setColorMode(checked ? `dark` : `light`);
+ };
+
+ return (
+
+
+
+
+
+ }
+ checkedIcon={
+
+
+
+
+
+ }
+ checked={isDark}
+ onChange={toggleColorMode}
+ onColor="#333"
+ offColor="#ddd"
+ {...props}
+ />
+ );
+};
diff --git a/ui/app-components/src/ColorMode/index.ts b/ui/app-components/src/ColorMode/index.ts
new file mode 100644
index 000000000..6d64c2112
--- /dev/null
+++ b/ui/app-components/src/ColorMode/index.ts
@@ -0,0 +1 @@
+export * from './ColorMode';
diff --git a/ui/app-components/src/Keyboard/Keyboard.tsx b/ui/app-components/src/Keyboard/Keyboard.tsx
new file mode 100644
index 000000000..d2dc02af4
--- /dev/null
+++ b/ui/app-components/src/Keyboard/Keyboard.tsx
@@ -0,0 +1,78 @@
+import {
+ cloneElement,
+ FC,
+ useCallback,
+ useEffect,
+ Children,
+ ReactElement,
+} from 'react';
+
+type KeyboardCallback = (key: number) => void;
+
+export interface KeyboardProps {
+ /**
+ * array of the keys to be trapped
+ */
+ keys: number[];
+ /**
+ * to where to attach the event handler
+ */
+ target?: 'children' | 'document' | 'window';
+ /**
+ * callbal on key down
+ */
+ onKeyDown: KeyboardCallback;
+ /**
+ * child element to the key event handler will be attached to if target = 'children'
+ */
+ children?: ReactElement;
+}
+
+export const LEFT_ARROW = 37;
+export const UP_ARROW = 38;
+export const RIGHT_ARROW = 39;
+export const DOWN_ARROW = 40;
+
+/**
+ * Componet to monitor keystrokes. Can attach to child, document or window.
+ */
+export const Keyboard: FC = ({
+ target = 'children',
+ keys,
+ onKeyDown,
+ children,
+}) => {
+ const onKeyDownFn = useCallback(
+ (event: KeyboardEvent) => {
+ const key = event.keyCode ? event.keyCode : event.which;
+ if (keys.includes(key)) {
+ event.preventDefault();
+ onKeyDown(key);
+ }
+ },
+ [keys, onKeyDown],
+ );
+
+ useEffect(() => {
+ if (target === 'document') {
+ document.addEventListener('keydown', onKeyDownFn);
+ } else if (target === 'window') {
+ window.addEventListener('keydown', onKeyDownFn);
+ }
+
+ return () => {
+ if (target === 'document') {
+ document.removeEventListener('keydown', onKeyDownFn);
+ } else if (target === 'window') {
+ window.removeEventListener('keydown', onKeyDownFn);
+ }
+ };
+ }, [onKeyDownFn, target]);
+ if (target === 'children' && children) {
+ return cloneElement(Children.only(children), {
+ onKeyDown: onKeyDownFn,
+ });
+ }
+
+ return children || null;
+};
diff --git a/ui/app-components/src/Keyboard/index.ts b/ui/app-components/src/Keyboard/index.ts
new file mode 100644
index 000000000..b60858865
--- /dev/null
+++ b/ui/app-components/src/Keyboard/index.ts
@@ -0,0 +1 @@
+export * from './Keyboard';
diff --git a/ui/app-components/src/Navmenu/Navmenu.stories.tsx b/ui/app-components/src/Navmenu/Navmenu.stories.tsx
new file mode 100644
index 000000000..9f8a09723
--- /dev/null
+++ b/ui/app-components/src/Navmenu/Navmenu.stories.tsx
@@ -0,0 +1,144 @@
+import React from 'react';
+import { Box, Badge, Input } from 'theme-ui';
+import Octicon, {
+ Inbox,
+ Mail,
+ Flame,
+ Star,
+ File,
+ Tag,
+ Trashcan,
+} from '@primer/octicons-react';
+import { Navmenu } from '.';
+
+export default {
+ title: 'App components/Navmenu',
+ component: Navmenu,
+};
+
+export const overview = () => (
+
+ console.log('onClick'),
+ label: 'F: drive',
+ },
+ ],
+ },
+ {
+ id: 'cloud',
+ label: 'Cloud',
+ items: [
+ { id: 'drop_box', href: '/drop_box', label: 'DropBox' },
+ {
+ id: 'google_drive',
+ onClick: () => console.log('onClick'),
+ label: 'Google drive',
+ },
+ ],
+ },
+ ]}
+ />
+
+);
+
+const navItems = [
+ {
+ id: 'inbox',
+ label: 'Inbox',
+ icon: ,
+ expanded: true,
+ items: [
+ {
+ id: 'all',
+ href: '/inbox/all',
+ label: 'All',
+ widget: 10,
+ },
+ {
+ id: 'gmail',
+ href: '/inbox/gmail',
+ label: 'GMail',
+ widget: 8,
+ },
+ {
+ id: 'work',
+ href: '/inbox/work',
+ label: 'Work',
+ widget: 1,
+ },
+ {
+ id: 'amazon',
+ href: '/inbox/amazon',
+ label: 'Amazon',
+ widget: 1,
+ },
+ ],
+ },
+ {
+ id: 'sent',
+ href: '/inbox/sent',
+ label: 'Sent',
+ icon: ,
+ },
+ {
+ id: 'flagged',
+ href: '/inbox/flagged',
+ label: 'Flagged',
+ icon: ,
+ widget: 3,
+ },
+ {
+ id: 'starred',
+ href: '/inbox/starred',
+ label: 'Starred',
+ icon: ,
+ },
+ {
+ id: 'drafts',
+ href: '/inbox/drafts',
+ label: 'Drafts',
+ icon: ,
+ },
+ {
+ id: 'tagged',
+ href: '/inbox/tagged',
+ label: 'Tagged',
+ icon: ,
+ },
+ {
+ id: 'trash',
+ href: '/inbox/trash',
+ label: 'Trash',
+ icon: ,
+ },
+];
+
+export const items = () => (
+
+
+
+);
+
+export const search = () => {
+ const [search, setSearch] = React.useState(undefined);
+
+ return (
+
+ setSearch(e.target.value)}
+ />
+
+
+ );
+};
diff --git a/ui/app-components/src/Navmenu/Navmenu.tsx b/ui/app-components/src/Navmenu/Navmenu.tsx
new file mode 100644
index 000000000..4fda31575
--- /dev/null
+++ b/ui/app-components/src/Navmenu/Navmenu.tsx
@@ -0,0 +1,349 @@
+/** @jsx jsx */
+import React, { FC, useEffect, useState } from 'react';
+import { jsx, Box, Flex, Button, ButtonProps, LinkProps, Text } from 'theme-ui';
+import Octicon, { PlusSmall, Dash } from '@primer/octicons-react';
+import {
+ Keyboard,
+ LEFT_ARROW,
+ UP_ARROW,
+ RIGHT_ARROW,
+ DOWN_ARROW,
+} from '../Keyboard';
+
+export interface MenuItem {
+ /** Unique id */
+ id?: string;
+ /** Label of the item */
+ label?: React.ReactNode;
+ /** Initial expanded state */
+ expanded?: boolean;
+ /** Icon in front of the label */
+ icon?: React.ReactNode;
+ /** Widget or icon to place at the end of the item */
+ widget?: React.ReactNode;
+ /** Array of child/sub-menu items */
+ items?: MenuItems;
+ /** href parameter for anchor type child elements */
+ href?: string;
+ /** event handler onClick */
+ onClick?: (id: string) => void;
+}
+
+export type MenuItems = MenuItem[];
+
+export type ButtonClassType =
+ | React.FunctionComponent
+ | React.FunctionComponent;
+
+export interface NavMenuProps {
+ /** Array of menu items */
+ items: MenuItems;
+ /** Initially active menu item */
+ activeItem?: {
+ id?: string;
+ label?: string;
+ };
+ /** Custom class to use for the button instead of Button */
+ buttonClass?: ButtonClassType;
+
+ /** If specified, will expand all items with chidren */
+ expandAll?: boolean;
+
+ /** Function that will be called when the user selects a menu item */
+ onSelect?: (item?: MenuItem) => void;
+
+ /** If specified, will filter the items by the search terms */
+ search?: string;
+}
+const isActive = (active: MenuItem, item: MenuItem): boolean =>
+ item.id === active.id || item.label === active.label;
+
+const hasActiveChidlren = (active: MenuItem, item: MenuItem): boolean => {
+ if (isActive(active, item)) {
+ return true;
+ }
+ return item.items
+ ? item.items.some(t => hasActiveChidlren(active, t))
+ : false;
+};
+
+const getExpandedItems = (children: MenuItems, active?: MenuItem): MenuItems =>
+ children.reduce((expandedItems: MenuItems, item: MenuItem) => {
+ const { items, expanded } = item;
+ if (expanded || (active && hasActiveChidlren(active, item))) {
+ expandedItems.push(item);
+ }
+ if (items) {
+ return expandedItems.concat(getExpandedItems(items, active));
+ }
+ return expandedItems;
+ }, []);
+
+const getCollapsibleItems = (children: MenuItems): MenuItems =>
+ children.reduce((collapsibleItems: MenuItems, item: MenuItem) => {
+ const { items } = item;
+ let childrenCollapsibleItems: MenuItems = [];
+ if (items) {
+ collapsibleItems.push(item);
+
+ childrenCollapsibleItems = getCollapsibleItems(items);
+ }
+ return collapsibleItems.concat(childrenCollapsibleItems);
+ }, []);
+
+const getFlatChildrenIds = (children?: MenuItems): MenuItems =>
+ children
+ ? children.reduce((flatChildren: MenuItems, item) => {
+ flatChildren.push(item);
+ if (item.items) {
+ // eslint-disable-next-line no-param-reassign
+ flatChildren = flatChildren.concat(getFlatChildrenIds(item.items));
+ }
+ return flatChildren;
+ }, [])
+ : [];
+
+const getChildrenById = (
+ children?: MenuItems,
+ id?: string,
+): MenuItems | undefined => {
+ if (!children || id) {
+ return undefined;
+ }
+
+ let items: MenuItems | undefined;
+ children.some(item => {
+ if (item.id === id || item.label === id) {
+ ({ items } = item);
+ return true;
+ }
+ if (item.items) {
+ items = getChildrenById(item.items, id);
+
+ if (items) {
+ return true;
+ }
+ }
+ return false;
+ });
+ return items;
+};
+
+const filterItems = (items: MenuItems, search?: string): MenuItems => {
+ if (search && search.length) {
+ const searchLC = search.toLowerCase();
+ return items
+ .map(item => Object.assign({}, item))
+ .filter(item => {
+ const { items: children, label } = item;
+ if (
+ typeof label === 'string' &&
+ label.toLowerCase().indexOf(searchLC) >= 0
+ ) {
+ return true;
+ }
+ if (children) {
+ const childItems = filterItems(children, search);
+ // eslint-disable-next-line no-param-reassign
+ item.items = childItems;
+ if (childItems.length) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+ return items;
+};
+
+interface NavMenuState {
+ expandedItems?: MenuItems;
+ originalExpandAll?: boolean;
+ search?: string;
+ items?: MenuItems;
+ filteredItems?: MenuItems;
+ collapsibleItems?: MenuItems;
+ allExpanded?: boolean;
+ expandAll?: boolean;
+}
+/**
+ * Hierarchical collapsible menu
+ */
+
+export const Navmenu: FC = props => {
+ const stateFromProps = ({
+ items,
+ expandAll,
+ activeItem,
+ search,
+ }: Pick) => {
+ const filteredItems = filterItems(items, search);
+ const collapsibleItems = getCollapsibleItems(filteredItems);
+ let expandedItems;
+ if (expandAll || (search && search.length)) {
+ expandedItems = collapsibleItems;
+ } else {
+ expandedItems = getExpandedItems(filteredItems, activeItem);
+ }
+
+ const allExpanded =
+ typeof expandAll !== 'undefined'
+ ? expandAll
+ : collapsibleItems.length === expandedItems.length;
+ return {
+ expandedItems,
+ items,
+ filteredItems,
+ search,
+ collapsibleItems,
+ allExpanded,
+ expandAll,
+ originalExpandAll: expandAll,
+ };
+ };
+
+ const [state, setState] = useState(stateFromProps(props));
+
+ useEffect(() => {
+ setState(
+ stateFromProps({
+ items: props.items,
+ expandAll: props.expandAll,
+ activeItem: props.activeItem,
+ search: props.search,
+ }),
+ );
+ }, [props.items, props.expandAll, props.activeItem, props.search]);
+
+ const onMenuChange = (item: MenuItem, expanded: boolean) => {
+ const { expandedItems, filteredItems } = state;
+
+ let newExpandedItems = [...(expandedItems || [])];
+ if (expanded) {
+ const id: string =
+ item.id || (typeof item.label === 'string' ? item.label : '');
+ const toBeCollapsed = [
+ id,
+ ...getFlatChildrenIds(getChildrenById(filteredItems, id)),
+ ];
+ newExpandedItems = newExpandedItems.filter(
+ item =>
+ toBeCollapsed.indexOf(
+ item.id || (typeof item.label === 'string' ? item.label : ''),
+ ) < 0,
+ );
+ } else {
+ newExpandedItems.push(item);
+ }
+
+ setState({
+ ...state,
+ expandedItems: newExpandedItems,
+ });
+ };
+
+ const renderItem = (item: MenuItem, level: number = 1) => {
+ const { activeItem, onSelect, buttonClass } = props;
+ const { expandedItems } = state;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { items, id, label, widget, icon, onClick, ...rest } = item;
+ const itemId = id || label;
+ const isExpanded: boolean =
+ expandedItems && itemId ? expandedItems.includes(item) : false;
+ const ButtonClass: ButtonClassType =
+ (items ? Button : buttonClass) || Button;
+ const itemKey = `item_${itemId}_${level}`;
+
+ let background;
+ if (activeItem && activeItem.id === id) {
+ background = 'active';
+ }
+
+ const content = (
+
+ {
+ if (items) {
+ onMenuChange(item, isExpanded);
+ } else if (typeof onSelect === 'function') {
+ onSelect(item);
+ }
+ }}
+ {...rest}
+ >
+
+ {items && (
+
+ )}
+
+
+ {icon && {icon}}
+ {typeof label === 'string' ? (
+
+ {items ? {label} : label}
+
+ ) : (
+ label
+ )}
+
+
+ {widget && {widget}}
+
+
+
+
+ );
+ return (
+
+ {items ? (
+
+ onMenuChange(item, [DOWN_ARROW, RIGHT_ARROW].includes(key))
+ }
+ >
+ {content}
+
+ ) : (
+ content
+ )}
+ {items &&
+ isExpanded &&
+ items.map(child => renderItem(child, level + 1))}
+
+ );
+ };
+ const { filteredItems } = state;
+ return (
+
+ {filteredItems && filteredItems.map(item => renderItem(item, 1))}
+
+ );
+};
diff --git a/ui/app-components/src/Navmenu/index.ts b/ui/app-components/src/Navmenu/index.ts
new file mode 100644
index 000000000..1f6eee808
--- /dev/null
+++ b/ui/app-components/src/Navmenu/index.ts
@@ -0,0 +1 @@
+export * from './Navmenu';
diff --git a/ui/app-components/src/Sidebar/Sidebar.stories.tsx b/ui/app-components/src/Sidebar/Sidebar.stories.tsx
new file mode 100644
index 000000000..71e7768b6
--- /dev/null
+++ b/ui/app-components/src/Sidebar/Sidebar.stories.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import Octicon, { Project } from '@primer/octicons-react';
+import { Flex } from 'theme-ui';
+import {
+ ThemeProvider,
+ CollapsibleProps,
+} from '@component-controls/components';
+import { Sidebar, SidebarContext, SidebarContextProvider } from '.';
+
+export default {
+ title: 'App components/Sidebar',
+ component: Sidebar,
+};
+
+export const overview = ({
+ collapsible,
+ width,
+ easing,
+}: {
+ collapsible: boolean;
+ width: number;
+ easing: CollapsibleProps['easing'];
+}) => (
+
+
+
+ {({ SidebarToggle }) => (
+
+
+
+
+ - item 1
+ - item 2
+ - item 3
+
+
+
+ )}
+
+
+
+);
+
+overview.story = {
+ controls: {
+ collapsible: { type: 'boolean', value: true },
+ width: { type: 'number', value: undefined },
+ easing: {
+ type: 'options',
+ options: ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out'],
+ },
+ },
+};
+
+export const title = () => (
+
+
+
+
+ {({ SidebarToggle }) => (
+
+
+
+
+
+ - item 1
+ - item 2
+ - item 3
+
+
+
+ )}
+
+
+
+
+);
+
+export const icon = () => (
+
+
+
+
+ {({ SidebarToggle }) => (
+
+ } />
+
+
+ - item 1
+ - item 2
+ - item 3
+
+
+
+ )}
+
+
+
+
+);
+
+export const buttonProps = () => (
+
+
+
+
+ {({ SidebarToggle }) => (
+
+
+
+
+ - item 1
+ - item 2
+ - item 3
+
+
+
+ )}
+
+
+
+
+);
diff --git a/ui/app-components/src/Sidebar/Sidebar.tsx b/ui/app-components/src/Sidebar/Sidebar.tsx
new file mode 100644
index 000000000..b172526df
--- /dev/null
+++ b/ui/app-components/src/Sidebar/Sidebar.tsx
@@ -0,0 +1,72 @@
+/** @jsx jsx */
+import React, { FC, Fragment, useContext } from 'react';
+import { jsx, Box, Flex, BoxProps, Heading } from 'theme-ui';
+import { useBreakpointIndex } from '@theme-ui/match-media';
+import { Collapsible, CollapsibleProps } from '@component-controls/components';
+
+import { SidebarContext } from './SidebarContext';
+
+export interface SidebarProps {
+ /**
+ * Title string or any react node
+ */
+ title?: React.ReactNode;
+
+ /** The width of the side bar in pixels */
+ width?: number;
+
+ /**
+ * Whether the sidebar can be collapsed
+ */
+ collapsible?: boolean;
+
+ /**
+ children content elements to be displayed in Sidebar
+ */
+ children: React.ReactNode;
+
+ /**
+ * collapsible animate height props
+ */
+ animate?: Omit;
+}
+
+/**
+ * Collapsible side bar component
+ */
+export const Sidebar: FC = ({
+ title,
+ width,
+ children,
+ animate,
+ ...rest
+}) => {
+ const toggleContext = useContext(SidebarContext);
+ const { collapsed, collapsible = true } = toggleContext || {};
+ const size: number = useBreakpointIndex();
+ const isCollapsed =
+ (collapsible && size <= 1 && collapsed === undefined) || collapsed === true;
+ return (
+
+
+
+
+ {title && (
+
+ {typeof title === 'string' ? (
+
+ {title}
+
+ ) : (
+ title
+ )}
+
+ )}
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/ui/app-components/src/Sidebar/SidebarContext.tsx b/ui/app-components/src/Sidebar/SidebarContext.tsx
new file mode 100644
index 000000000..2404764ea
--- /dev/null
+++ b/ui/app-components/src/Sidebar/SidebarContext.tsx
@@ -0,0 +1,53 @@
+import React, { FC, createContext } from 'react';
+import { Button, ButtonProps } from 'theme-ui';
+import Octicon, { ThreeBars } from '@primer/octicons-react';
+
+export type SidebarToggleProps = {
+ icon?: React.ReactNode;
+} & ButtonProps;
+
+export interface SidebarContextProps {
+ SidebarToggle: FC;
+ collapsed?: boolean;
+ collapsible?: boolean;
+ setCollapsed: (value: boolean) => void;
+}
+export const SidebarContext = createContext({
+ SidebarToggle: () => null,
+ setCollapsed: () => {},
+});
+export interface SidebarContextProviderProps {
+ collapsible?: boolean;
+}
+export const SidebarContextProvider: FC = ({
+ children,
+ collapsible = true,
+}) => {
+ const [collapsed, setCollapsed] = React.useState(
+ undefined,
+ );
+ const SidebarToggle: FC = ({ icon, ...rest }) => {
+ console.log(collapsible);
+ return collapsible ? (
+
+ ) : null;
+ };
+ return (
+
+ {children}
+
+ );
+};
diff --git a/ui/app-components/src/Sidebar/index.ts b/ui/app-components/src/Sidebar/index.ts
new file mode 100644
index 000000000..e9a228462
--- /dev/null
+++ b/ui/app-components/src/Sidebar/index.ts
@@ -0,0 +1,2 @@
+export * from './Sidebar';
+export * from './SidebarContext';
diff --git a/ui/app-components/src/index.ts b/ui/app-components/src/index.ts
new file mode 100644
index 000000000..2f5f7ba4a
--- /dev/null
+++ b/ui/app-components/src/index.ts
@@ -0,0 +1,4 @@
+export * from './ColorMode';
+export * from './Keyboard';
+export * from './Navmenu';
+export * from './Sidebar';
diff --git a/ui/app-components/src/typings.d.ts b/ui/app-components/src/typings.d.ts
new file mode 100644
index 000000000..d4ceab75b
--- /dev/null
+++ b/ui/app-components/src/typings.d.ts
@@ -0,0 +1 @@
+declare module '@theme-ui/match-media';
diff --git a/ui/app-components/tsconfig.json b/ui/app-components/tsconfig.json
new file mode 100644
index 000000000..e69184809
--- /dev/null
+++ b/ui/app-components/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "esnext",
+ "outDir": "dist",
+ "rootDir": "src",
+ "declaration": true,
+ "types": ["node", "jest"],
+ "typeRoots": ["../../node_modules/@types", "node_modules/@types"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules/**"]
+}
\ No newline at end of file
diff --git a/ui/blocks/README.md b/ui/blocks/README.md
index a50586bfd..2e1a1e55d 100644
--- a/ui/blocks/README.md
+++ b/ui/blocks/README.md
@@ -37,7 +37,7 @@ Some of the guiding design goals for this library:
# List of components
-
+
@@ -172,12 +172,10 @@ _PageContainer [source code](https:/github.com/ccontrols/component-controls/tree
| Name | Type | Description |
| ------------ | ----------------------- | -------------------------------------------------------------------------------------- |
+| `store*` | _StoryStore_ | store object |
| `storyId` | _string_ | story to display in the page |
-| `dark` | _boolean_ | dark/light theme for the page |
| `options` | _any_ | global options passed from container those are global parameters as well as decorators |
| `components` | _MDXProviderComponents_ | components to customize the markdown display. |
-| `theme` | _Theme_ | optional custom theme |
-| `store` | _StoryStore_ | store object |
## Playground
@@ -326,7 +324,7 @@ _StorySource [source code](https:/github.com/ccontrols/component-controls/tree/m
| Name | Type | Description |
| ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `viewStype` | _ViewStyle_ | initial view mode |
+| `viewStyle` | _ViewStyle_ | initial view mode |
| `id` | _string_ | id of the story optional id to be used for the block if no id is provided, one will be calculated automatically from the title. |
| `name` | _string_ | alternatively you can use the name of a story to load from an external file |
| `title` | _string_ | optional section title for the block. |
diff --git a/ui/blocks/src/PageContainer/PageContainer.tsx b/ui/blocks/src/PageContainer/PageContainer.tsx
index 4c8997433..77a0fc085 100644
--- a/ui/blocks/src/PageContainer/PageContainer.tsx
+++ b/ui/blocks/src/PageContainer/PageContainer.tsx
@@ -73,6 +73,7 @@ export const PageContainer: FC = ({
return (
= ({
return applyColorMode(
{
...defTheme,
+ defaultBreakpoints: [40, 52, 64].map(n => n + 'em'),
initialColorModeName: dark ? 'dark' : 'default',
forms: {
checkbox: {
diff --git a/yarn.lock b/yarn.lock
index 5785a1a4f..06b895756 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3871,6 +3871,11 @@
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.3.1.tgz#b85c7e8fae948dc0de65aa30b853368993e25cb3"
integrity sha512-QB2/fZBpo4inaLHL3OrB8NOBgNfwnj8GtHzXWHb9iQSRjmtNX8zPXBe32jLT7qQP0+y8JxPT4YChZIkm5ZyIdg==
+"@theme-ui/match-media@^0.3.1":
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/@theme-ui/match-media/-/match-media-0.3.1.tgz#05af6a71cf14368e1b3bd7180fc382c72d5ba53b"
+ integrity sha512-PHvSRB1vqUgDnPkGlXLa+qadmOMOy3LKSOzovwpTi+wzCUyyOGAsUY/fJQ7nufBrmU3vdYeUTrKplLn5VIEmlg==
+
"@theme-ui/mdx@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.3.0.tgz#8bb1342204acfaa69914d6b6567c5c49d9a8c1e6"