├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── run-tests.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── changelog.md
├── docs
├── global.d.ts
├── index.html
├── public
│ └── favicon.ico
├── src
│ ├── components
│ │ ├── Content.ts
│ │ ├── InterfaceTable.ts
│ │ ├── Main.ts
│ │ ├── MethodsTable.ts
│ │ ├── Nav.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── logo.svg
│ ├── style.scss
│ └── utils
│ │ ├── highlightCode.ts
│ │ └── normalizeDocs.ts
├── tsconfig.json
└── vite.config.ts
├── package-lock.json
├── package.json
├── readme.md
├── scripts
└── generateIcons.js
├── src
├── _shared
│ ├── _colors.scss
│ ├── _mixins.scss
│ ├── _utility.scss
│ ├── _variables.scss
│ ├── align.ts
│ ├── attrs.ts
│ ├── classes.ts
│ ├── colors.ts
│ ├── examples
│ │ ├── ContentSelect.ts
│ │ ├── Example.ts
│ │ ├── IntentSelect.ts
│ │ ├── PopoverPositionSelect.ts
│ │ ├── SizeSelect.ts
│ │ ├── _index.scss
│ │ ├── countries.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── intents.ts
│ ├── keys.ts
│ ├── responsive.ts
│ ├── sizes.ts
│ ├── test
│ │ └── utils.ts
│ └── utils.ts
├── components
│ ├── abstract-component
│ │ └── index.ts
│ ├── base-control
│ │ ├── index.scss
│ │ └── index.ts
│ ├── breadcrumb
│ │ ├── Breadcrumb.ts
│ │ ├── BreadcrumbItem.ts
│ │ ├── breadcrumb.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── button-group
│ │ ├── button-group.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── button
│ │ ├── button.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── callout
│ │ ├── callout.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── card
│ │ ├── card.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── checkbox
│ │ ├── checkbox.md
│ │ ├── examples
│ │ │ ├── controlled.ts
│ │ │ ├── default.ts
│ │ │ ├── indeterminate.ts
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── collapse
│ │ ├── collapse.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ └── index.ts
│ ├── control-group
│ │ ├── control-group.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── custom-select
│ │ ├── custom-select.md
│ │ ├── examples
│ │ │ ├── default.ts
│ │ │ ├── index.ts
│ │ │ └── itemRender.ts
│ │ ├── index.scss
│ │ └── index.ts
│ ├── dialog
│ │ ├── dialog.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── drawer
│ │ ├── drawer.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── empty-state
│ │ ├── empty-state.md
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── form
│ │ ├── Form.ts
│ │ ├── FormGroup.ts
│ │ ├── FormLabel.ts
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── form.md
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── grid
│ │ ├── Col.ts
│ │ ├── Grid.ts
│ │ ├── examples
│ │ │ ├── basic.ts
│ │ │ ├── index.ts
│ │ │ ├── offset.ts
│ │ │ ├── order.ts
│ │ │ └── responsive.ts
│ │ ├── grid.md
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── icon
│ │ ├── Icon.ts
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── icon.md
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── index.md
│ ├── index.scss
│ ├── index.ts
│ ├── input-file
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── input-file.md
│ ├── input-popover
│ │ ├── _index.scss
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── input-popover.md
│ ├── input-select
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── input-select.md
│ ├── input
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── input.md
│ ├── list
│ │ ├── List.ts
│ │ ├── ListItem.ts
│ │ ├── examples
│ │ │ ├── complex.ts
│ │ │ ├── index.ts
│ │ │ ├── nested.ts
│ │ │ └── simple.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── list.md
│ ├── menu
│ │ ├── Menu.ts
│ │ ├── MenuDivider.ts
│ │ ├── MenuHeading.ts
│ │ ├── MenuItem.ts
│ │ ├── examples
│ │ │ ├── default.ts
│ │ │ ├── index.ts
│ │ │ └── submenu.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── menu.md
│ ├── overlay
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── overlay.md
│ ├── popover-menu
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── popover-menu.md
│ ├── popover
│ │ ├── Popover.ts
│ │ ├── examples
│ │ │ ├── controlled.ts
│ │ │ ├── default.ts
│ │ │ ├── index.ts
│ │ │ └── nested.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ ├── popover.md
│ │ └── popoverTypes.ts
│ ├── portal
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── portal.md
│ ├── query-list
│ │ ├── examples
│ │ │ ├── controlled.ts
│ │ │ ├── default.ts
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── query-list.md
│ ├── radio
│ │ ├── Radio.ts
│ │ ├── RadioGroup.ts
│ │ ├── examples
│ │ │ ├── default.ts
│ │ │ ├── index.ts
│ │ │ └── radio-group.ts
│ │ ├── index.scss
│ │ ├── index.ts
│ │ └── radio.md
│ ├── select-list
│ │ ├── examples
│ │ │ ├── default.ts
│ │ │ ├── index.ts
│ │ │ └── multiple.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── select-list.md
│ ├── select
│ │ ├── examples
│ │ │ ├── controlled.ts
│ │ │ ├── default.ts
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── select.md
│ ├── spinner
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── spinner.md
│ ├── switch
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── switch.md
│ ├── table
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── table.md
│ ├── tabs
│ │ ├── Tabs.ts
│ │ ├── TabsItem.ts
│ │ ├── _index.scss
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── tabs.md
│ ├── tag-input
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── tag-input.md
│ ├── tag
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── tag.md
│ ├── text-area
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── text-area.md
│ ├── toast
│ │ ├── Toast.ts
│ │ ├── Toaster.ts
│ │ ├── examples
│ │ │ ├── declarative.ts
│ │ │ ├── default.ts
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── toast.md
│ ├── tooltip
│ │ ├── examples
│ │ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.ts
│ │ └── tooltip.md
│ └── tree
│ │ ├── Tree.ts
│ │ ├── TreeNode.ts
│ │ ├── examples
│ │ ├── data.ts
│ │ └── index.ts
│ │ ├── index.scss
│ │ ├── index.spec.ts
│ │ ├── index.ts
│ │ └── tree.md
├── core
│ ├── _nav.md
│ ├── colors.md
│ ├── examples
│ │ ├── colors.ts
│ │ ├── icons.ts
│ │ └── index.ts
│ ├── getting-started.md
│ ├── icons.md
│ ├── introduction.md
│ ├── reset.scss
│ ├── typography.md
│ └── typography.scss
├── examples.ts
├── index.scss
├── index.ts
└── utils
│ ├── focus-manager
│ ├── examples
│ │ └── index.ts
│ ├── focus-manager.md
│ ├── index.scss
│ └── index.ts
│ ├── index.ts
│ ├── responsive-manager
│ ├── examples
│ │ └── index.ts
│ ├── index.ts
│ └── responsive-manager.md
│ ├── transition-manager
│ ├── examples
│ │ └── index.ts
│ ├── index.ts
│ └── transition-manager.md
│ └── utils.md
├── test
├── setup.ts
├── tsconfig.json
└── vite.config.ts
├── tsconfig.cjs.json
├── tsconfig.esm.json
├── tsconfig.json
└── vite.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | indent_size = 2
10 | indent_style = space
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | public
4 | generated
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true
5 | },
6 | extends: [],
7 | parser: '@typescript-eslint/parser',
8 | parserOptions: {
9 | ecmaVersion: 'latest',
10 | sourceType: 'module'
11 | },
12 | plugins: [
13 | '@typescript-eslint'
14 | ],
15 | rules: {
16 | '@typescript-eslint/explicit-module-boundary-types': 'off',
17 | '@typescript-eslint/no-explicit-any': 'off',
18 | '@typescript-eslint/no-unused-vars': 'off',
19 | 'camelcase': 'error',
20 | 'comma-dangle': 'warn',
21 | 'quotes': ['error', 'single'],
22 | 'no-console': 'error',
23 | 'no-multi-spaces': 'warn',
24 | 'no-unused-vars': 'off',
25 | 'semi': 'error'
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [16.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 |
25 | - run: npm ci
26 | - run: npm run generate:icons
27 | - run: npm test
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules
3 | generated
4 | dist
5 | lib
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | docs
4 | scripts
5 | test
6 | .editorconfig
7 | .travis.yml
8 | vite.config.js
9 | .eslintrc.js
10 | .eslintignore
11 | tsconfig*
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Vasil Rimar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare const VERSION: String;
3 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= APP_TITLE %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrimar/construct-ui/5a0d5b6bcd500c9b6beb1b6ea8791227013d58c5/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/src/components/Content.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { IDocumentationData } from '..';
3 | import { InterfaceTable } from './InterfaceTable';
4 | import { MethodsTable } from './MethodsTable';
5 | import * as Examples from '../../../src/examples';
6 | import { FocusManager, ResponsiveManager } from '@/';
7 | import { ITag, INavigable } from '@documentalist/client';
8 | import { highlightCode } from '../utils/highlightCode';
9 |
10 | FocusManager.showFocusOnlyOnTab();
11 | ResponsiveManager.initialize();
12 |
13 | const examples = Examples as any;
14 |
15 | export function Content(attrs: IDocumentationData) {
16 | const pageData = attrs.docs.pages[attrs.page];
17 |
18 | const contentAttrs = {
19 | class: [
20 | 'Docs-content',
21 | `Docs-content-${pageData.reference}`
22 | ].join(' '),
23 | key: attrs.page,
24 | id: attrs.page,
25 | oncreate: () => highlightCode()
26 | };
27 |
28 | return m('section', contentAttrs, [
29 | pageData.contents.map((contentBlock => {
30 | if (typeof (contentBlock) === 'string') {
31 | return m.trust(contentBlock);
32 | }
33 |
34 | const content = contentBlock as ITag & INavigable;
35 |
36 | switch (content.tag) {
37 | case 'heading':
38 | return m(`h${content.level}`, content.value);
39 | case 'methods':
40 | return MethodsTable({ api: content.value, data: attrs });
41 | case 'interface':
42 | return InterfaceTable({ api: content.value, data: attrs });
43 | case 'example':
44 | const example = examples[content.value];
45 | return example ? m(example) : 'RENDER ERROR';
46 | }
47 | }))
48 | ]);
49 | }
50 |
--------------------------------------------------------------------------------
/docs/src/components/Main.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Content, Nav } from './';
3 | import { IDocumentationData } from '..';
4 | import { ResponsiveManager, Drawer, Icons, Button } from '@/';
5 | import logoSrc from '../logo.svg';
6 |
7 | export class Main implements m.Component {
8 | private isDrawerOpen: boolean = false;
9 | private scrollPosition: number;
10 |
11 | public view({ attrs }: m.Vnode) {
12 | const isMobile = ResponsiveManager.is('xs') || ResponsiveManager.is('sm');
13 |
14 | const logo = m('img.Docs-logo', { src: logoSrc });
15 |
16 | const nav = Nav({
17 | data: attrs,
18 | closeDrawer: this.closeDrawer,
19 | logo,
20 | isMobile,
21 | onLinkClick: this.handleLinkClick
22 | });
23 |
24 | return m('.Docs', { class: isMobile ? 'is-mobile' : '' }, [
25 | isMobile && m('.Docs-mobile-header', [
26 | logo,
27 | m(Button, {
28 | basic: true,
29 | iconLeft: Icons.MENU,
30 | label: 'Menu',
31 | onclick: () => this.isDrawerOpen = true,
32 | size: 'xs'
33 | })
34 | ]),
35 |
36 | isMobile
37 | ? m(Drawer, {
38 | class: 'Docs-nav-drawer',
39 | isOpen: this.isDrawerOpen,
40 | position: 'left',
41 | content: nav,
42 | onClose: this.closeDrawer,
43 | onOpened: this.handleDrawerOnOpened
44 | })
45 | : nav,
46 |
47 | m('.Docs-container', Content({ ...attrs }))
48 | ]);
49 | }
50 |
51 | private closeDrawer = () => this.isDrawerOpen = false;
52 |
53 | private handleLinkClick = (e: Event) => {
54 | const target = e.target as HTMLElement;
55 | const contentEl = target.closest('.Docs-nav');
56 | this.scrollPosition = contentEl!.scrollTop;
57 | };
58 |
59 | private handleDrawerOnOpened = (el: HTMLElement) => {
60 | const contentEl = el.querySelector('.Docs-nav');
61 | contentEl!.scrollTop = this.scrollPosition;
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/docs/src/components/MethodsTable.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { IDocumentationData } from '..';
3 | import { Table } from '@/';
4 | import { ITsMethod, ITsInterface } from '@documentalist/client';
5 |
6 | export interface IMethodTableAttrs {
7 | data: IDocumentationData;
8 | api: string;
9 | }
10 |
11 | export function MethodsTable(attrs: IMethodTableAttrs) {
12 | const { data, api } = attrs;
13 | const methods = (data.docs.typescript[api] as ITsInterface).methods;
14 |
15 | return m(Table, [
16 | m('tr', [
17 | m('th[style=min-width:220px]', 'Name'),
18 | m('th', 'Type'),
19 | m('th', 'Description')
20 | ]),
21 | methods.map(renderRow)
22 | ]);
23 | }
24 |
25 | function renderRow(method: ITsMethod) {
26 | const signature = method.signatures[0];
27 |
28 | return m('tr', [
29 | m('td', m('code', method.name)),
30 | m('td', signature.type),
31 | m('td', m.trust(signature.documentation!.contentsRaw))
32 | ]);
33 | }
34 |
--------------------------------------------------------------------------------
/docs/src/components/Nav.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { IDocumentationData } from '..';
3 |
4 | export interface INavAttrs {
5 | data: IDocumentationData;
6 | closeDrawer: Function;
7 | logo: m.Vnode;
8 | isMobile: boolean;
9 | onLinkClick: (e: Event) => void;
10 | }
11 |
12 | export function Nav(attrs: INavAttrs) {
13 | const { data, isMobile, closeDrawer, logo, onLinkClick } = attrs;
14 |
15 | return m('.Docs-nav', { class: isMobile ? 'is-mobile' : '' }, m('.Docs-nav-content', [
16 | m('.Docs-nav-header', [
17 | logo,
18 | m('', [
19 | m('.Docs-nav-title', 'Construct-ui'),
20 | m('.Docs-nav-title-meta', [
21 | m('a', {
22 | href: 'https://github.com/vrimar/construct-ui',
23 | target: '_blank'
24 | }, 'Github'),
25 | m('.Docs-nav-version', `(v${VERSION})`)
26 | ])
27 | ])
28 | ]),
29 |
30 | data.docs.nav.map(heading => m('.Docs-nav-section', [
31 | m('h4', heading.title),
32 |
33 | heading.children.map(child => [
34 | m('a', {
35 | class: `/${child.route}` === m.route.get() ? 'is-active' : '',
36 | onclick: (e: Event) => {
37 | onLinkClick(e);
38 | closeDrawer();
39 | m.route.set(`#/${child.route}`);
40 | },
41 | href: `#/${child.route}`
42 | }, child.title)
43 | ])
44 | ]))
45 | ]));
46 | }
47 |
--------------------------------------------------------------------------------
/docs/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Content';
2 | export * from './Main';
3 | export * from './Nav';
4 | export * from './InterfaceTable';
5 | export * from './MethodsTable';
6 |
--------------------------------------------------------------------------------
/docs/src/index.ts:
--------------------------------------------------------------------------------
1 | import './style.scss';
2 | import m from 'mithril';
3 | import { Main } from './components/Main';
4 | import { IMarkdownPluginData, ITypescriptPluginData } from '@documentalist/client';
5 | import { normalizeDocs } from './utils/normalizeDocs';
6 | import json from '../generated/docs.json';
7 |
8 | export type Data = IMarkdownPluginData & ITypescriptPluginData;
9 |
10 | const docs = normalizeDocs(json as any);
11 |
12 | export interface IDocumentationData {
13 | docs: IMarkdownPluginData & ITypescriptPluginData;
14 | page: string;
15 | requestedPath: string;
16 | }
17 |
18 | export const GITHUB_ROOT = 'https://github.com/vasilrimar/construct-ui';
19 | export const DEFAULT_ROOT = '/introduction/getting-started';
20 |
21 | m.route.prefix = '#';
22 |
23 | const resolveRoute = (wrapper: m.Component) => ({
24 | onmatch(attrs, requestedPath) {
25 | attrs.docs = docs;
26 | attrs.requestedPath = requestedPath;
27 |
28 | window.requestAnimationFrame(() => window.scrollTo(0, 0));
29 | },
30 | render({ attrs }) {
31 | return m(wrapper, { ...attrs });
32 | }
33 | }) as m.RouteResolver;
34 |
35 | const initRoutes = (layout: any) => {
36 | m.route(document.getElementById('Docs')!, DEFAULT_ROOT, {
37 | '/introduction/:page': resolveRoute(layout),
38 | '/components/:page...': resolveRoute(layout),
39 | '/utils/:page...': resolveRoute(layout)
40 | });
41 | };
42 |
43 | initRoutes(Main);
44 |
--------------------------------------------------------------------------------
/docs/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/utils/highlightCode.ts:
--------------------------------------------------------------------------------
1 | import highlightjs from 'highlight.js';
2 | import 'highlight.js/styles/github.css';
3 |
4 | export function highlightCode() {
5 | const blocks = document.querySelectorAll('pre code');
6 |
7 | Array.from(blocks).map(block => {
8 | highlightjs.highlightBlock(block as HTMLElement);
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/docs/src/utils/normalizeDocs.ts:
--------------------------------------------------------------------------------
1 | import { Data } from '..';
2 |
3 | export function normalizeDocs(data: Data) {
4 | Object.keys(data.typescript).map(key => {
5 | const prop = data.typescript[key];
6 |
7 | if (prop.kind === 'interface' || prop.kind === 'class') {
8 | prop.properties.sort((a, b) => {
9 | const textA = a.name;
10 | const textB = b.name;
11 | return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
12 | });
13 |
14 | prop.methods.sort((a, b) => {
15 | const textA = a.name;
16 | const textB = b.name;
17 | return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
18 | });
19 |
20 | prop.properties.map(attr => {
21 | const type = attr.type;
22 |
23 | if (type === 'undefined | false | true') {
24 | attr.type = 'boolean';
25 | }
26 |
27 | if (type === 'undefined | string') {
28 | attr.type = 'string';
29 | }
30 |
31 | if (type === 'undefined | number') {
32 | attr.type = 'number';
33 | }
34 | });
35 | }
36 |
37 | if (prop.kind === 'enum') {
38 | prop.members = prop.members
39 | .filter(member => !member.name.includes('NONE') && !member.name.includes('DEFAULT'));
40 | }
41 | });
42 |
43 | return data;
44 | }
45 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "module": "ES2020",
6 | "resolveJsonModule": true
7 | },
8 | "include": [
9 | "./src",
10 | "./global.d.ts"
11 | ],
12 | "exclude": [
13 | "../node_modules"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/docs/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, PluginOption } from 'vite';
2 | import tsconfigPaths from 'vite-tsconfig-paths';
3 | import checker from 'vite-plugin-checker';
4 | import packageJson from '../package.json';
5 |
6 | const transformHtmlPlugin = (data: any) => ({
7 | name: 'transform-html',
8 | transformIndexHtml: {
9 | enforce: 'pre',
10 | transform: html => html.replace(
11 | /<%=\s*(\w+)\s*%>/gi,
12 | (_, p1) => data[p1] || ''
13 | )
14 | }
15 | }) as PluginOption;
16 |
17 | export default defineConfig({
18 | define: {
19 | VERSION: JSON.stringify(packageJson.version)
20 | },
21 | plugins: [
22 | transformHtmlPlugin({
23 | APP_TITLE: `Construct-ui: ${packageJson.description} - v${packageJson.version}`
24 | }),
25 | tsconfigPaths(),
26 | checker({
27 | typescript: true
28 | })
29 | ]
30 | });
31 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 |
5 | Construct-UI
6 |
7 |
8 |
9 | ### A [Mithril.js](https://github.com/MithrilJS/mithril.js) UI library.
10 |
11 | [](https://www.npmjs.com/package/construct-ui)
12 | 
13 | [](https://gitter.im/construct-ui/Lobby)
14 |
15 |
16 |
17 | ## Documentation
18 | Check out the [documentation website](https://vrimar.github.io/construct-ui) for installation instructions and getting started.
19 |
20 | ## Playground
21 | Check out [codesandbox](https://codesandbox.io/s/x7zzjovzyz) examples for basic usage.
22 |
23 | ## Development
24 | 1. `npm i`
25 | 2. `npm run generate:icons`
26 | 3. `npm run generate:docs`
27 | 4. `npm run watch:docs`
28 |
29 | ## Credits
30 | Inspired by [blueprint](https://github.com/palantir/blueprint), [polythene](https://github.com/ArthurClemens/polythene) and [ant-design](https://github.com/ant-design/ant-design).
31 |
--------------------------------------------------------------------------------
/scripts/generateIcons.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const ICONS = require('feather-icons/dist/icons.json');
4 |
5 | const GENERATED_ICON_PATH = path.resolve(process.cwd(), './src/components/icon/generated');
6 |
7 | if (!fs.existsSync(GENERATED_ICON_PATH)) {
8 | fs.mkdirSync(GENERATED_ICON_PATH);
9 | }
10 |
11 | async function writeLinesToFile(filename, ...lines) {
12 | const outputPath = path.join(GENERATED_ICON_PATH, filename);
13 | const contents = ['/* eslint:disable */', ...lines, ''].join('\n');
14 | fs.writeFileSync(outputPath, contents);
15 | }
16 |
17 | function exportIconNames() {
18 | return Object.keys(ICONS).map(iconName => {
19 | const constName = iconName.replace(/-/g, '_').toUpperCase();
20 | return `export const ${constName} = '${iconName}';`;
21 | });
22 | }
23 |
24 | function exportIconContents() {
25 | const contents = Object.keys(ICONS).map(iconName => `'${iconName}' : '${ICONS[iconName]}'\n`);
26 | return `export default {
27 | ${contents}
28 | }`;
29 | }
30 |
31 | function generateIndex() {
32 | return `import * as Icons from './IconNames';
33 | import IconContents from './IconContents';
34 |
35 | export { Icons, IconContents };
36 | `;
37 | }
38 |
39 | writeLinesToFile('IconNames.ts', ...exportIconNames());
40 | writeLinesToFile('IconContents.ts', exportIconContents());
41 | writeLinesToFile('index.ts', generateIndex());
42 |
--------------------------------------------------------------------------------
/src/_shared/_utility.scss:
--------------------------------------------------------------------------------
1 | .cui-align-right {
2 | display: flex;
3 | justify-content: flex-end;
4 | }
5 |
6 | .cui-align-left {
7 | display: flex;
8 | justify-content: flex-start;
9 | }
10 |
11 | .cui-fluid {
12 | width:100%;
13 | }
14 |
15 | .cui-disabled {
16 | cursor: not-allowed !important;
17 | opacity: 0.65 !important;
18 | }
19 |
20 | .cui-hidden {
21 | display: none;
22 | }
23 |
--------------------------------------------------------------------------------
/src/_shared/align.ts:
--------------------------------------------------------------------------------
1 | export const Align = {
2 | LEFT: 'left',
3 | CENTER: 'center',
4 | RIGHT: 'right'
5 | } as const;
6 |
7 | export type Align = typeof Align[keyof typeof Align];
8 |
--------------------------------------------------------------------------------
/src/_shared/attrs.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { IconName, Intent, Size } from '..';
3 | import { IIconAttrs } from '../components/icon';
4 |
5 | export type Style = string | Partial;
6 |
7 | export interface IAttrs {
8 | /** Space delimited class list */
9 | class?: string;
10 |
11 | /** Inline styles */
12 | style?: Style;
13 | }
14 |
15 | export interface IIntentAttrs {
16 | /** Component color intent */
17 | intent?: Intent;
18 | }
19 |
20 | export interface ISizeAttrs {
21 | /** Component size */
22 | size?: Size;
23 | }
24 |
25 | export interface IActionItemAttrs {
26 | /** Toggles active state */
27 | active?: boolean;
28 |
29 | /** Disables interaction */
30 | disabled?: boolean;
31 |
32 | /** Inner text or children */
33 | label?: m.Children;
34 |
35 | /** Left-justified icon */
36 | iconLeft?: IconName;
37 |
38 | /** Attrs passed though to left-justified icon */
39 | iconLeftAttrs?: Partial;
40 |
41 | /** Right-justified icon */
42 | iconRight?: IconName;
43 |
44 | /** Attrs passed though to right-justified icon */
45 | iconRightAttrs?: Partial;
46 |
47 | /** Callback invoked on click */
48 | onclick?: (e: Event) => void;
49 | }
50 |
51 | export interface IOption {
52 | /** Disables interaction */
53 | disabled?: boolean;
54 |
55 | /** Inner text */
56 | label?: string | number;
57 |
58 | /** Value of option */
59 | value?: string | number;
60 | }
61 |
62 | export type Option = IOption | string | number;
63 |
--------------------------------------------------------------------------------
/src/_shared/examples/ContentSelect.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Select, IconName, Icon, Button, Spinner, Tag, getObjectKeys } from '../../';
3 |
4 | export const ContentType = {
5 | NONE: 'none',
6 | ICON: 'Icon',
7 | BUTTON: 'Button',
8 | SPINNER: 'Spinner',
9 | TAG: 'Tag'
10 | } as const;
11 |
12 | export type ContentType = typeof ContentType[keyof typeof ContentType];
13 |
14 | export interface IContentSelectAttrs {
15 | onSelect: (contentType?: ContentType) => void;
16 | }
17 |
18 | export class ContentSelect implements m.Component {
19 | public view({ attrs: { onSelect } }: m.Vnode) {
20 | return m(Select, {
21 | fluid: true,
22 | options: getObjectKeys(ContentType).map(key => ContentType[key]),
23 | onchange: (e: Event) => {
24 | const target = (e.target as HTMLSelectElement);
25 | const content = target.options[target.selectedIndex].value as ContentType;
26 | onSelect(content === 'none' ? undefined : content);
27 | },
28 | size: 'xs'
29 | });
30 | }
31 | }
32 |
33 | export function renderContent(contentType: ContentType, icon: IconName) {
34 | if (contentType === 'Icon') {
35 | return m(Icon, { name: icon });
36 | }
37 |
38 | if (contentType === 'Button') {
39 | return m(Button, { label: 'Button' });
40 | }
41 |
42 | if (contentType === 'Spinner') {
43 | return m(Spinner, { active: true });
44 | }
45 |
46 | if (contentType === 'Tag') {
47 | return m(Tag, { label: 'Tag' });
48 | }
49 |
50 | if (contentType === 'none') {
51 | return undefined;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/_shared/examples/IntentSelect.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { getObjectKeys, Intent, ISelectAttrs, Select } from '../../';
3 |
4 | export interface IIntentSelectAttrs extends ISelectAttrs {
5 | onSelect: (intent?: Intent) => void;
6 | }
7 |
8 | export class IntentSelect implements m.Component {
9 | public view({ attrs: { onSelect, ...otherAttrs } }: m.Vnode) {
10 | return m(Select, {
11 | ...otherAttrs,
12 | options: getObjectKeys(Intent).map(key => Intent[key]),
13 | onchange: (e: Event) => {
14 | const target = (e.target as HTMLSelectElement);
15 | const intent = target.options[target.options.selectedIndex].value as Intent;
16 | onSelect(intent === 'none' ? undefined : intent);
17 | },
18 | size: 'xs',
19 | fluid: true
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/_shared/examples/PopoverPositionSelect.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { PopoverPosition, ISelectAttrs, Select, getObjectKeys } from '../../';
3 |
4 | export interface IPopoverPositionSelectAttrs extends ISelectAttrs {
5 | onSelect: (e: PopoverPosition) => void;
6 | }
7 |
8 | export class PopoverPositionSelect implements m.Component {
9 | public view({ attrs: { onSelect, ...otherAttrs } }: m.Vnode) {
10 | return m(Select, {
11 | ...otherAttrs,
12 | fluid: true,
13 | options: getObjectKeys(PopoverPosition).map(key => PopoverPosition[key]),
14 | onchange: (e: Event) => {
15 | const target = (e.target as HTMLSelectElement);
16 | const position = target.options[target.options.selectedIndex].value as PopoverPosition;
17 | onSelect(position);
18 | },
19 | size: 'xs'
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/_shared/examples/SizeSelect.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { getObjectKeys, ISelectAttrs, Select, Size } from '../../';
3 |
4 | export interface ISizeSelectAttrs extends ISelectAttrs {
5 | onSelect: (size?: Size) => void;
6 | }
7 |
8 | export class SizeSelect implements m.Component {
9 | private selected: Size = 'default';
10 |
11 | public view({ attrs }: m.Vnode) {
12 | const { onSelect, ...otherAttrs } = attrs;
13 |
14 | return m(Select, {
15 | ...otherAttrs,
16 | fluid: true,
17 | options: getObjectKeys(Size).map(key => Size[key]),
18 | onchange: (e: Event) => {
19 | const target = (e.target as HTMLSelectElement);
20 | const size = target.options[target.selectedIndex].value as Size;
21 | this.selected = size;
22 | onSelect(size === 'default' ? undefined : size);
23 | },
24 | size: 'xs',
25 | value: this.selected
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/_shared/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './countries';
2 | export * from './ContentSelect';
3 | export * from './IntentSelect';
4 | export * from './PopoverPositionSelect';
5 | export * from './SizeSelect';
6 | export * from './Example';
7 |
--------------------------------------------------------------------------------
/src/_shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from './align';
2 | export * from './attrs';
3 | export { Classes } from './classes';
4 | export { Colors } from './colors';
5 | export * from './intents';
6 | export * from './keys';
7 | export * from './responsive';
8 | export * from './sizes';
9 | export * from './utils';
10 |
--------------------------------------------------------------------------------
/src/_shared/intents.ts:
--------------------------------------------------------------------------------
1 | export const Intent = {
2 | NONE: 'none',
3 | PRIMARY: 'primary',
4 | NEGATIVE: 'negative',
5 | POSITIVE: 'positive',
6 | WARNING: 'warning'
7 | } as const;
8 |
9 | export type Intent = typeof Intent[keyof typeof Intent];
10 |
--------------------------------------------------------------------------------
/src/_shared/keys.ts:
--------------------------------------------------------------------------------
1 | export const Keys = {
2 | TAB: 9,
3 | ENTER: 13,
4 | SHIFT: 16,
5 | ESCAPE: 27,
6 | SPACE: 32,
7 | ARROW_LEFT: 37,
8 | ARROW_UP: 38,
9 | ARROW_RIGHT: 39,
10 | ARROW_DOWN: 40
11 | };
12 |
13 | export type Keys = typeof Keys[keyof typeof Keys];
14 |
--------------------------------------------------------------------------------
/src/_shared/responsive.ts:
--------------------------------------------------------------------------------
1 | export const Breakpoints = {
2 | xs: '(max-width: 575.98px)',
3 | sm: '(min-width: 576px) and (max-width: 767.98px)',
4 | md: '(min-width: 768px) and (max-width: 991.98px)',
5 | lg: '(min-width: 992px) and (max-width: 1199.98px)',
6 | xl: '(min-width: 1200px)'
7 | } as const;
8 |
9 | export type Breakpoint = keyof typeof Breakpoints;
10 |
--------------------------------------------------------------------------------
/src/_shared/sizes.ts:
--------------------------------------------------------------------------------
1 | export const Size = {
2 | XS: 'xs',
3 | SM: 'sm',
4 | DEFAULT: 'default',
5 | LG: 'lg',
6 | XL: 'xl'
7 | } as const;
8 |
9 | export type Size = typeof Size[keyof typeof Size];
10 |
--------------------------------------------------------------------------------
/src/_shared/test/utils.ts:
--------------------------------------------------------------------------------
1 | export const TIMEOUT = 70;
2 |
3 | export function hasClass(el: HTMLElement, className: string) {
4 | return el.classList.contains(className);
5 | }
6 |
7 | export function hasChildClass(el: HTMLElement, className: string) {
8 | return el.querySelector(`.${className}`);
9 | }
10 |
11 | export async function triggerEvent(el: Element, type: string) {
12 | await sleep(TIMEOUT);
13 | el.dispatchEvent(new Event(type, { bubbles: true }));
14 | await sleep(TIMEOUT);
15 | }
16 |
17 | export async function keyboardEvent(el: HTMLElement, key: number) {
18 | el.dispatchEvent(new KeyboardEvent('keydown', { which: key, bubbles: true } as any));
19 | await sleep(TIMEOUT);
20 | }
21 |
22 | export const sleep = (milliseconds: number = 0) => new Promise(r => setTimeout(r, milliseconds));
23 |
--------------------------------------------------------------------------------
/src/components/abstract-component/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 |
3 | export abstract class AbstractComponent implements m.Component {
4 | protected timeoutStack: number[] = [];
5 | protected attrs: A = {} as A;
6 | protected prevAttrs: A;
7 |
8 | public abstract view(vnode: m.Vnode): m.Children | null | void;
9 | public abstract getDefaultAttrs(): A;
10 |
11 | public oninit(vnode: m.Vnode) {
12 | vnode.attrs = vnode.attrs || {} as A;
13 | this.setAttrs(vnode);
14 | }
15 |
16 | public onbeforeupdate(vnode: m.Vnode, prev: m.VnodeDOM) {
17 | this.setAttrs(vnode);
18 | this.prevAttrs = prev.attrs;
19 | }
20 |
21 | private setAttrs(vnode: m.Vnode) {
22 | vnode.attrs = this.getAttrs(vnode.attrs);
23 | this.attrs = vnode.attrs;
24 | }
25 |
26 | private getAttrs(attrs: A): A {
27 | return {
28 | ...this.getDefaultAttrs() as Object,
29 | ...attrs as Object
30 | } as A;
31 | }
32 |
33 | protected setTimeout = (callback: () => void, timeout?: number) => {
34 | const handle = window.setTimeout(callback, timeout);
35 | this.timeoutStack.push(handle);
36 | return () => window.clearTimeout(handle);
37 | };
38 |
39 | protected clearTimeouts = () => {
40 | if (this.timeoutStack.length) {
41 | this.timeoutStack.map((timeout) => clearTimeout(timeout));
42 | this.timeoutStack = [];
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/base-control/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 | @import '../../_shared/mixins';
3 |
4 |
5 | @mixin cui-control-style($text-color) {
6 | color: $text-color;
7 | }
8 |
9 | .cui-control {
10 | @include cui-control-sizing($cui-control-base, $cui-font-size);
11 | @include cui-control-style($cui-text-color);
12 |
13 | position: relative;
14 | display: inline-block;
15 | vertical-align: top;
16 | user-select: none;
17 | cursor: pointer;
18 |
19 | input {
20 | position:absolute;
21 | top:0;
22 | left:0;
23 | z-index: -1;
24 | opacity: 0;
25 | }
26 |
27 | .cui-control-indicator {
28 | background: white;
29 | position:absolute;
30 | top:0;
31 | left:0;
32 | margin:0;
33 | border-radius: 2px;
34 | transition: background $cui-transition-duration $cui-transition-ease;
35 | }
36 |
37 | input:focus ~ .cui-control-indicator {
38 | @include focus-outline();
39 | }
40 |
41 | &:hover .cui-control-indicator,
42 | &:focus .cui-control-indicator {
43 | background: $cui-base-bg-color-hover;
44 | border: solid 1px $cui-base-border-color-hover;
45 | }
46 |
47 | &:active .cui-control-indicator {
48 | background: $cui-base-bg-color-active;
49 | border: solid 1px $cui-base-border-color-active;
50 | }
51 |
52 | &.cui-disabled .cui-control-indicator {
53 | background: white;
54 | user-select: none;
55 | cursor: not-allowed;
56 | opacity: 0.5;
57 | }
58 |
59 | @each $size in $cui-sizes {
60 | &.cui-#{$size} {
61 | @include cui-control-sizing(
62 | map-get($cui-control-map, $size),
63 | map-get($cui-font-size-map, $size)
64 | )
65 | }
66 | }
67 |
68 | @each $intent in $cui-intents {
69 | &.cui-#{$intent} {
70 | @include cui-control-style(map-get($cui-bg-color-map, $intent));
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/base-control/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 |
5 | export interface IControlAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
6 | /** Toggles checked state */
7 | checked?: boolean;
8 |
9 | /**
10 | * Attrs passed through to container element
11 | */
12 | containerAttrs?: any;
13 |
14 | /** Initially sets control to checked state (uncontrolled mode) */
15 | defaultChecked?: boolean;
16 |
17 | /** Disables interaction */
18 | disabled?: boolean;
19 |
20 | /** Text label */
21 | label?: m.Children;
22 |
23 | /** Callback invoked on control change */
24 | onchange?: (e: Event) => void;
25 |
26 | /** Disables interaction but maintains styling */
27 | readonly?: boolean;
28 |
29 | type?: 'checkbox' | 'radio';
30 |
31 | typeClass?: string;
32 |
33 | [htmlAttrs: string]: any;
34 | }
35 |
36 | export class BaseControl implements m.Component {
37 | public view({ attrs }: m.Vnode) {
38 | const {
39 | class: className,
40 | containerAttrs = {},
41 | intent,
42 | label,
43 | size,
44 | type,
45 | typeClass,
46 | style,
47 | ...htmlAttrs
48 | } = attrs;
49 |
50 | const classes = classnames(
51 | Classes.CONTROL,
52 | typeClass,
53 | htmlAttrs.disabled && Classes.DISABLED,
54 | intent && `cui-${intent}`,
55 | size && `cui-${size}`,
56 | className
57 | );
58 |
59 | const content = [
60 | m('input', {
61 | ...htmlAttrs,
62 | disabled: htmlAttrs.disabled || htmlAttrs.readonly,
63 | type
64 | }),
65 | m(`span.${Classes.CONTROL_INDICATOR}`),
66 | label
67 | ];
68 |
69 | return m('label', {
70 | class: classes,
71 | style,
72 | ...containerAttrs
73 | }, content);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/Breadcrumb.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs, ISizeAttrs } from '../../_shared';
4 |
5 | export interface IBreadcrumbAttrs extends IAttrs, ISizeAttrs {
6 | /** Element to display in between breadcrumb items */
7 | seperator?: m.Child;
8 |
9 | [htmlAttrs: string]: any;
10 | }
11 |
12 | export class Breadcrumb implements m.Component {
13 | public view({ attrs, children }: m.Vnode) {
14 | const { class: className, seperator = '/', size, ...htmlAttrs } = attrs;
15 |
16 | const classes = classnames(
17 | Classes.BREADCRUMB,
18 | size && `cui-${size}`,
19 | className
20 | );
21 |
22 | return m('', {
23 | ...htmlAttrs,
24 | class: classes
25 | }, this.renderChildren(children as m.ChildArray, attrs));
26 | }
27 |
28 | private renderChildren(children: m.ChildArray, { seperator }: IBreadcrumbAttrs) {
29 | return children
30 | .filter((item) => item != null)
31 | .map((item) => [
32 | item,
33 | m(`span.${Classes.BREADCRUMB_SEPERATOR}`, seperator)
34 | ]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/BreadcrumbItem.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs } from '../../_shared';
4 |
5 | export interface IBreadcrumbItemAttrs extends IAttrs {
6 | [htmlAttrs: string]: any;
7 | }
8 |
9 | export class BreadcrumbItem implements m.Component {
10 | public view({ attrs, children }: m.Vnode) {
11 | const { class: className, ...htmlAttrs } = attrs;
12 | const tag = htmlAttrs.href != null ? 'a' : 'span';
13 | const classes = classnames(Classes.BREADCRUMB_ITEM, className);
14 |
15 | return m(tag, { ...htmlAttrs, class: classes }, children);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/breadcrumb.md:
--------------------------------------------------------------------------------
1 | @# Breadcrumbs
2 | Breadcrumbs display the current location within a hierarchy.
3 |
4 | @example BreadcrumbExample
5 |
6 | @## Breadcrumb Attrs
7 | @interface IBreadcrumbAttrs
8 |
9 | @## BreadcrumbItem Attrs
10 | @interface IBreadcrumbItemAttrs
11 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Breadcrumb, BreadcrumbItem, Icon, Icons, Size } from '@/';
3 | import { SizeSelect, Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/breadcrumb/examples/index.ts';
6 |
7 | export class BreadcrumbExample {
8 | private size: Size;
9 |
10 | public view() {
11 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
12 | m(Breadcrumb, {
13 | size: this.size,
14 | seperator: m(Icon, { name: Icons.CHEVRON_RIGHT })
15 | }, [
16 | m(BreadcrumbItem, { href: '#' }, m(Icon, { name: Icons.HOME })),
17 | m(BreadcrumbItem, { href: '#' }, 'Application'),
18 | m(BreadcrumbItem, 'Section 1')
19 | ])
20 | ]);
21 | }
22 |
23 | private renderOptions() {
24 | return [
25 | m('h5', 'Size'),
26 | m(SizeSelect, { onSelect: (size: Size) => this.size = size })
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/_variables';
2 |
3 | @mixin cui-breadcrumb-sizing($padding, $font-size) {
4 | .cui-breadcrumb-item {
5 | font-size: $font-size;
6 | }
7 |
8 | .cui-breadcrumb-seperator {
9 | font-size: $font-size;
10 | margin: 0 $padding * 0.5;
11 | }
12 | }
13 |
14 | .cui-breadcrumb {
15 | @include cui-breadcrumb-sizing($cui-base-padding, $cui-font-size);
16 |
17 | display:flex;
18 | align-items: center;
19 |
20 | .cui-breadcrumb-item {
21 | display: flex;
22 | text-decoration: none;
23 | color: $blue-grey500;
24 | transition: color $cui-transition-duration $cui-transition-ease;
25 |
26 | .cui-icon {
27 | color: $blue-grey500;;
28 | }
29 | }
30 |
31 | a.cui-breadcrumb-item, {
32 | color: $blue-grey200;
33 |
34 | .cui-icon { color: $blue-grey200 }
35 |
36 | &:hover {
37 | color: $blue-grey500;
38 |
39 | .cui-icon { color: $blue-grey500 }
40 | }
41 | }
42 |
43 | .cui-breadcrumb-seperator {
44 | display: flex;
45 | color: $blue-grey500;
46 |
47 | &:last-child {
48 | display:none;
49 | }
50 | }
51 |
52 | @each $size in $cui-sizes {
53 | &.cui-#{$size} {
54 | @include cui-breadcrumb-sizing(
55 | map-get($cui-padding-map, $size),
56 | map-get($cui-font-size-map, $size)
57 | )
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, beforeEach, expect, it } from 'vitest';
3 | import { Classes, Breadcrumb, BreadcrumbItem, IBreadcrumbAttrs, Icon, Icons } from '@/';
4 | import { hasChildClass, hasClass } from '@test-utils';
5 |
6 | describe('breadcrumb', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | beforeEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | size: 'xs'
15 | });
16 |
17 | expect(hasClass(el(), Classes.BREADCRUMB)).toBeTruthy();
18 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
19 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
20 | });
21 |
22 | it('Renders children', () => {
23 | mount({});
24 |
25 | const childrenLength = el().querySelectorAll(`.${Classes.BREADCRUMB_ITEM}`).length;
26 | const seperatorLength = el().querySelectorAll(`.${Classes.BREADCRUMB_SEPERATOR}`).length;
27 |
28 | expect(childrenLength).toBe(2);
29 | expect(seperatorLength).toBe(2);
30 | });
31 |
32 | it('Passes through html attrs', () => {
33 | mount({
34 | id: 1,
35 | name: 'name'
36 | });
37 |
38 | expect(el().hasAttribute('id')).toBeTruthy();
39 | expect(el().hasAttribute('name')).toBeTruthy();
40 | });
41 |
42 | it('Renders custom seperator', () => {
43 | mount({
44 | seperator: m(Icon, { name: Icons.ACTIVITY })
45 | });
46 |
47 | expect(hasChildClass(el(), `${Classes.ICON}-${Icons.ACTIVITY}`)).toBeTruthy();
48 | });
49 |
50 | function mount(attrs: IBreadcrumbAttrs) {
51 | const component = {
52 | view: () => m(Breadcrumb, { ...attrs }, [
53 | m(BreadcrumbItem, 'label'),
54 | m(BreadcrumbItem, 'label')
55 | ])
56 | };
57 | m.mount(document.body, component);
58 | }
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/breadcrumb/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Breadcrumb';
2 | export * from './BreadcrumbItem';
3 |
--------------------------------------------------------------------------------
/src/components/button-group/button-group.md:
--------------------------------------------------------------------------------
1 | @# Button Group
2 |
3 | A button group arranges multiple buttons in a horizontal format.
4 |
5 | @example ButtonGroupExample
6 |
7 | @## ButtonGroup Attrs
8 | @interface IButtonGroupAttrs
9 |
--------------------------------------------------------------------------------
/src/components/button-group/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/_variables';
2 |
3 | .cui-button-group {
4 | display: inline-flex;
5 | vertical-align: middle;
6 |
7 | .cui-button {
8 | flex: 1 0 auto;
9 | position:relative;
10 | margin:0;
11 |
12 | &:not(:first-child) {
13 | border-top-left-radius: 0;
14 | border-bottom-left-radius:0;
15 | }
16 |
17 | &:not(:last-child) {
18 | margin-right: -1px;
19 | border-top-right-radius: 0;
20 | border-bottom-right-radius:0;
21 | }
22 |
23 | &:active,
24 | &.cui-active,
25 | &:focus,
26 | &:hover {
27 | z-index: 10;
28 | }
29 | }
30 |
31 | &.cui-basic .cui-button {
32 | margin-right: $cui-base-size;
33 | border-radius: $cui-border-radius;
34 |
35 | &:not(:last-child):after {
36 | margin: $cui-base-size * 0.5;
37 | background: $cui-base-border-color;
38 | width: $cui-border-width;
39 | display: inline-block;
40 | position: absolute;
41 | top: 5%;
42 | bottom: 5%;
43 | left: 100%;
44 | content: "";
45 | }
46 | }
47 |
48 | &.cui-fluid {
49 | width:100%;
50 |
51 | .cui-button {
52 | flex: 1 1 auto;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/button-group/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, beforeEach, expect, it } from 'vitest';
3 | import { ButtonGroup, Classes, IButtonGroupAttrs, Button } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('button-group', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | beforeEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | basic: true,
14 | class: Classes.POSITIVE,
15 | fluid: true,
16 | intent: 'primary',
17 | rounded: true,
18 | outlined: true,
19 | size: 'xs',
20 | style: 'margin: 0'
21 | });
22 |
23 | expect(hasClass(el(), Classes.BUTTON_GROUP)).toBeTruthy();
24 | expect(hasClass(el(), Classes.BASIC)).toBeTruthy();
25 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
26 | expect(hasClass(el(), Classes.FLUID)).toBeTruthy();
27 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
28 | expect(hasClass(el(), Classes.ROUNDED)).toBeTruthy();
29 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
30 |
31 | expect(el().hasAttribute('style')).toBeTruthy();
32 | });
33 |
34 | it('Renders children', () => {
35 | mount({});
36 |
37 | const length = el().children.length;
38 | expect(length).toBe(2);
39 | });
40 |
41 | it('Passes through html attrs', () => {
42 | mount({
43 | id: 1,
44 | name: 'name'
45 | });
46 |
47 | expect(el().hasAttribute('id')).toBeTruthy();
48 | expect(el().hasAttribute('name')).toBeTruthy();
49 | });
50 |
51 | function mount(attrs: IButtonGroupAttrs) {
52 | const component = {
53 | view: () => m(ButtonGroup, { ...attrs }, [
54 | m(Button, { label: 'label' }),
55 | m(Button, { label: 'label' })
56 | ])
57 | };
58 |
59 | m.mount(document.body, component);
60 | }
61 | });
62 |
--------------------------------------------------------------------------------
/src/components/button-group/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 |
5 | export interface IButtonGroupAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
6 | /** Toggles basic styling on children (no borders/background) */
7 | basic?: boolean;
8 |
9 | /** Adds rounded styling (no borders/background) */
10 | rounded?: boolean;
11 |
12 | /** Toggles outline styling on children (no background) */
13 | outlined?: boolean;
14 |
15 | /** Fills width of parent container */
16 | fluid?: boolean;
17 |
18 | [htmlAttrs: string]: any;
19 | }
20 |
21 | export class ButtonGroup implements m.Component {
22 | public view({ attrs, children }: m.Vnode) {
23 | const { class: className, size, fluid, intent, rounded, outlined, basic, ...htmlAttrs } = attrs;
24 |
25 | return m('', {
26 | ...htmlAttrs,
27 | class: classnames(
28 | Classes.BUTTON_GROUP,
29 | rounded && Classes.ROUNDED,
30 | fluid && Classes.FLUID,
31 | basic && Classes.BASIC,
32 | outlined && Classes.OUTLINED,
33 | intent && `cui-${intent}`,
34 | size && `cui-${size}`,
35 | className
36 | )
37 | }, children);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/button/button.md:
--------------------------------------------------------------------------------
1 | @# Button
2 | A button triggers an action.
3 |
4 | @example ButtonExample
5 |
6 | @## Button Attrs
7 | @interface IButtonAttrs
8 |
--------------------------------------------------------------------------------
/src/components/callout/callout.md:
--------------------------------------------------------------------------------
1 | @# Callout
2 | A callout highlights important information.
3 |
4 | @example CalloutExample
5 |
6 | @## Callout Attrs
7 | @interface ICalloutAttrs
8 |
--------------------------------------------------------------------------------
/src/components/callout/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, beforeEach, expect, it } from 'vitest';
3 | import { Callout, Classes, ICalloutAttrs, Icons } from '@/';
4 | import { hasClass, hasChildClass } from '@test-utils';
5 |
6 | describe('callout', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | beforeEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | intent: 'primary',
15 | size: 'xs',
16 | style: 'margin: 0'
17 | });
18 |
19 | expect(hasClass(el(), Classes.CALLOUT)).toBeTruthy();
20 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
21 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
22 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
23 |
24 | expect(el().hasAttribute('style')).toBeTruthy();
25 | });
26 |
27 | it('Renders children', () => {
28 | mount({
29 | content: 'test',
30 | header: 'test',
31 | icon: Icons.ACTIVITY
32 | });
33 |
34 | expect(hasClass(el(), Classes.CALLOUT_ICON)).toBeTruthy();
35 | expect(hasChildClass(el(), Classes.CALLOUT_CONTENT)).toBeTruthy();
36 | expect(hasChildClass(el(), Classes.CALLOUT_HEADER)).toBeTruthy();
37 | expect(hasChildClass(el(), Classes.ICON)).toBeTruthy();
38 | });
39 |
40 | it('Passes through html attrs', () => {
41 | mount({
42 | id: 1,
43 | name: 'name'
44 | });
45 |
46 | expect(el().hasAttribute('id')).toBeTruthy();
47 | expect(el().hasAttribute('name')).toBeTruthy();
48 | });
49 |
50 | it('Renders dismiss icon when onDismiss set', () => {
51 | mount({ onDismiss: () => null });
52 |
53 | expect(hasChildClass(el(), Classes.CALLOUT_DISMISS_ICON)).toBeTruthy();
54 | });
55 |
56 | function mount(attrs: ICalloutAttrs) {
57 | const component = {
58 | view: () => m(Callout, { ...attrs })
59 | };
60 | m.mount(document.body, component);
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/src/components/callout/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { IAttrs, ISizeAttrs, IIntentAttrs, Classes } from '../../_shared';
4 | import { Icon, IconName, Icons } from '../icon';
5 |
6 | export interface ICalloutAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
7 | /** Inner text content */
8 | content?: m.Children;
9 |
10 | /**
11 | * Callback invoked when "dismiss" icon is clicked;
12 | * Omitting this property will hide the dismiss icon.
13 | */
14 | onDismiss?: (e: Event) => void;
15 |
16 | /** Header content */
17 | header?: m.Children;
18 |
19 | /** Left-justified icon */
20 | icon?: IconName;
21 |
22 | [htmlAttrs: string]: any;
23 | }
24 |
25 | export class Callout implements m.Component {
26 | public view({ attrs }: m.Vnode) {
27 | const { content, header, icon, intent, onDismiss, size, ...htmlAttrs } = attrs;
28 |
29 | const innerContent = [
30 | onDismiss && m(Icon, {
31 | class: Classes.CALLOUT_DISMISS_ICON,
32 | name: Icons.X,
33 | onclick: onDismiss
34 | }),
35 | icon && m(Icon, { name: icon }),
36 | header && m(`.${Classes.CALLOUT_HEADER}`, header),
37 | content && m(`.${Classes.CALLOUT_CONTENT}`, content)
38 | ];
39 |
40 | const classes = classnames(
41 | Classes.CALLOUT,
42 | icon && Classes.CALLOUT_ICON,
43 | intent && `cui-${attrs.intent}`,
44 | size && `cui-${attrs.size}`,
45 | attrs.class
46 | );
47 |
48 | return m('', { ...htmlAttrs, class: classes }, innerContent);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/card/card.md:
--------------------------------------------------------------------------------
1 | @# Card
2 | A card displays a bordered section of content.
3 |
4 | @example CardExample
5 |
6 | @## Card Attrs
7 | @interface ICardAttrs
8 |
--------------------------------------------------------------------------------
/src/components/card/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Card, RadioGroup, Switch, Size } from '@/';
3 | import { Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/card/examples/index.ts';
6 | const elevations = ['0', '1', '2', '3', '4'];
7 |
8 | export class CardExample {
9 | private elevation = 1;
10 | private fluid: boolean;
11 | private interactive = false;
12 | private size: Size;
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), center: true, src: EXAMPLE_SRC }, [
16 | m(Card, {
17 | elevation: this.elevation,
18 | fluid: this.fluid,
19 | interactive: this.interactive,
20 | size: this.size,
21 | style: 'min-width: 300px'
22 | },
23 | m('h4', 'Card title'),
24 | m('', 'Card content')
25 | )
26 | ]);
27 | }
28 |
29 | private renderOptions() {
30 | return [
31 | m('h5', 'Size'),
32 | m(SizeSelect, {
33 | value: this.size,
34 | onSelect: (size: Size) => this.size = size
35 | }),
36 | m(Switch, {
37 | label: 'Interactive',
38 | onchange: () => this.interactive = !this.interactive
39 | }),
40 | m(Switch, {
41 | label: 'Fluid',
42 | onchange: () => this.fluid = !this.fluid
43 | }),
44 | m('h5', 'Elevation'),
45 | m(RadioGroup, {
46 | options: elevations,
47 | value: this.elevation.toString(),
48 | onchange: (e: Event) => this.elevation = parseInt((e.currentTarget as HTMLInputElement).value)
49 | })
50 | ];
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/card/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/_mixins';
2 | @import '../../_shared/_variables';
3 |
4 | @mixin cui-card-sizing($padding, $font-size) {
5 | padding: floor($padding * 1.25);
6 | }
7 |
8 | .cui-card {
9 | @include cui-card-sizing($cui-base-padding, $cui-font-size);
10 |
11 | background:$white;
12 | max-width: 320px;
13 | border-radius: $cui-border-radius;
14 | border: solid 1px $cui-base-border-color;
15 | transition: box-shadow $cui-transition-duration ease-in-out;
16 |
17 | @each $size in $cui-sizes {
18 | &.cui-#{$size} {
19 | @include cui-card-sizing(
20 | map-get($cui-padding-map, $size),
21 | map-get($cui-font-size-map, $size)
22 | )
23 | }
24 | }
25 |
26 | &.cui-elevation-1 {
27 | box-shadow: $cui-elevation-1;
28 | }
29 |
30 | &.cui-elevation-2 {
31 | box-shadow: $cui-elevation-2;
32 | }
33 |
34 | &.cui-elevation-3 {
35 | box-shadow: $cui-elevation-3;
36 | }
37 |
38 | &.cui-elevation-4 {
39 | box-shadow: $cui-elevation-4;
40 | }
41 |
42 | &.cui-card-interactive {
43 | cursor: pointer;
44 |
45 | &:hover, &:focus {
46 | box-shadow: $cui-elevation-3;
47 | }
48 |
49 | &:active {
50 | box-shadow: $cui-elevation-1;
51 | }
52 | }
53 |
54 | &.cui-fluid {
55 | max-width: none;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/card/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, beforeEach, expect, it } from 'vitest';
3 | import { Card, Classes, ICardAttrs } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('card', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | beforeEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | elevation: 2,
15 | fluid: true,
16 | interactive: true,
17 | size: 'xs',
18 | style: 'margin: 0'
19 | });
20 |
21 | expect(hasClass(el(), Classes.CARD)).toBeTruthy();
22 | expect(hasClass(el(), Classes.CARD_INTERACTIVE)).toBeTruthy();
23 | expect(hasClass(el(), Classes.FLUID)).toBeTruthy();
24 | expect(hasClass(el(), `${Classes.ELEVATION}-2`)).toBeTruthy();
25 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
26 | expect(el().hasAttribute('style')).toBeTruthy();
27 | });
28 |
29 | it('Passes through html attrs', () => {
30 | mount({ id: 1, name: 'name' });
31 |
32 | expect(el().hasAttribute('id')).toBeTruthy();
33 | expect(el().hasAttribute('name')).toBeTruthy();
34 | });
35 |
36 | function mount(attrs: ICardAttrs) {
37 | const component = {
38 | view: () => m(Card, { ...attrs })
39 | };
40 |
41 | m.mount(document.body, component);
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/card/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs } from '../../_shared';
4 |
5 | export interface ICardAttrs extends IAttrs, ISizeAttrs {
6 | /** Degree of card shadow */
7 | elevation?: number;
8 |
9 | /** Fills width of parent container */
10 | fluid?: boolean;
11 |
12 | /** Adds interactive hover/active styling */
13 | interactive?: boolean;
14 |
15 | [htmlAttrs: string]: any;
16 | }
17 |
18 | export class Card implements m.Component {
19 | public view({ attrs, children }: m.Vnode) {
20 | const {
21 | class: className,
22 | elevation,
23 | fluid,
24 | interactive,
25 | size,
26 | ...htmlAttrs
27 | } = attrs;
28 |
29 | return m('', {
30 | ...htmlAttrs,
31 | class: classnames(
32 | Classes.CARD,
33 | elevation && `cui-elevation-${elevation || 1}`,
34 | fluid && Classes.FLUID,
35 | interactive && Classes.CARD_INTERACTIVE,
36 | size && `cui-${size}`,
37 | className
38 | )
39 | }, children);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/checkbox/checkbox.md:
--------------------------------------------------------------------------------
1 | @# Checkbox
2 | A checkbox is a form control that is either in a checked, unchecked or indeterminate state.
3 | @example CheckboxExample
4 |
5 | @## Controlled checkbox
6 | A "controlled" checkbox can only be toggled through external state.
7 | @example CheckboxControlledExample
8 |
9 | @## Indeterminate state
10 | A checkbox can be in a "partially checked" or indeterminate state.
11 | @example CheckboxIndeterminateExample
12 |
13 | @## Checkbox Attrs
14 | @interface ICheckboxAttrs
15 |
--------------------------------------------------------------------------------
/src/components/checkbox/examples/controlled.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Checkbox, Switch } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/checkbox/examples/controlled.ts';
6 |
7 | export class CheckboxControlledExample {
8 | private checked = false;
9 |
10 | public view() {
11 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
12 | m(Checkbox, {
13 | checked: this.checked,
14 | label: 'Controlled Checkbox',
15 | onchange: () => null
16 | })
17 | ]);
18 | }
19 |
20 | private renderOptions() {
21 | return [
22 | m(Switch, {
23 | checked: this.checked,
24 | label: 'Checked',
25 | onchange: () => this.checked = !this.checked
26 | })
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/checkbox/examples/default.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Checkbox, Switch, Size, Intent } from '@/';
3 | import { IntentSelect, Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/checkbox/examples/default.ts';
6 |
7 | export class CheckboxExample {
8 | private disabled = false;
9 | private intent: Intent;
10 | private label = true;
11 | private readonly = false;
12 | private size: Size;
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
16 | m(Checkbox, {
17 | defaultChecked: true,
18 | disabled: this.disabled,
19 | label: this.label && 'Checkbox label',
20 | intent: this.intent,
21 | readonly: this.readonly,
22 | size: this.size
23 | })
24 | ]);
25 | }
26 |
27 | private renderOptions() {
28 | return [
29 | m('h5', 'Size'),
30 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
31 | m('h5', 'Intent'),
32 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
33 | m(Switch, {
34 | checked: this.disabled,
35 | label: 'Disabled',
36 | onchange: () => this.disabled = !this.disabled
37 | }),
38 |
39 | m(Switch, {
40 | checked: this.label,
41 | label: 'Label',
42 | onchange: () => this.label = !this.label
43 | }),
44 |
45 | m(Switch, {
46 | checked: this.readonly,
47 | label: 'Readonly',
48 | onchange: () => this.readonly = !this.readonly
49 | })
50 | ];
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/checkbox/examples/indeterminate.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Checkbox } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/checkbox/examples/indeterminate.ts';
6 |
7 | export class CheckboxIndeterminateExample {
8 | private options = ['First', 'Second', 'Third'];
9 | private checkedOptions = ['First', 'Second'];
10 |
11 | public view() {
12 | const { checkedOptions, options } = this;
13 | const indeterminate = checkedOptions.length > 0 && checkedOptions.length < options.length;
14 | const checked = checkedOptions.length === options.length;
15 |
16 | return m(Example, { direction: 'column', src: EXAMPLE_SRC }, [
17 | m('', [
18 | m(Checkbox, {
19 | checked,
20 | indeterminate,
21 | label: 'Indeterminate checkbox',
22 | onchange: (e: Event) => this.onCheckAll(e),
23 | style: 'display:block; margin-bottom: 15px'
24 | }),
25 |
26 | this.options.map((option) => m(Checkbox, {
27 | label: option,
28 | checked: checkedOptions.includes(option),
29 | onchange: (e: Event) => this.onChange(e, option),
30 | style: 'display:block; margin-bottom: 10px'
31 | }))
32 | ])
33 | ]);
34 | }
35 |
36 | private onCheckAll(e: Event) {
37 | this.checkedOptions = (e.target as HTMLInputElement).checked ? [...this.options] : [];
38 | }
39 |
40 | private onChange(e: Event, option: string) {
41 | if ((e.target as HTMLInputElement).checked) {
42 | this.checkedOptions.push(option);
43 | } else {
44 | const index = this.checkedOptions.indexOf(option);
45 | this.checkedOptions.splice(index, 1);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/checkbox/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controlled';
2 | export * from './default';
3 | export * from './indeterminate';
4 |
--------------------------------------------------------------------------------
/src/components/checkbox/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Classes } from '../../_shared';
3 | import { BaseControl, IControlAttrs } from '../base-control';
4 |
5 | export interface ICheckboxAttrs extends IControlAttrs {
6 | /** Initially sets control to indeterminate state (uncontrolled mode) */
7 | defaultIndeterminate?: boolean;
8 |
9 | /** Toggles indeterminate state */
10 | indeterminate?: boolean;
11 | }
12 |
13 | export class Checkbox implements m.Component {
14 | private input: HTMLInputElement;
15 |
16 | public oncreate({ attrs, dom }: m.VnodeDOM) {
17 | this.input = dom.querySelector('input') as HTMLInputElement;
18 |
19 | if (attrs.defaultIndeterminate != null) {
20 | this.input.indeterminate = attrs.defaultIndeterminate;
21 | }
22 | this.updateIndeterminate(attrs);
23 | }
24 |
25 | public onupdate({ attrs, dom }: m.VnodeDOM) {
26 | this.input = dom.querySelector('input') as HTMLInputElement;
27 | this.updateIndeterminate(attrs);
28 | }
29 |
30 | public view({ attrs }: m.Vnode) {
31 | return m(BaseControl, {
32 | ...attrs as IControlAttrs,
33 | type: 'checkbox',
34 | typeClass: Classes.CHECKBOX
35 | });
36 | }
37 |
38 | private updateIndeterminate(attrs: ICheckboxAttrs) {
39 | if (attrs.indeterminate != null) {
40 | this.input.indeterminate = attrs.indeterminate;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/collapse/collapse.md:
--------------------------------------------------------------------------------
1 | @# Collapse
2 | A collapse toggles content via a slide-in/out animation.
3 |
4 | @example CollapseExample
5 |
6 | @## Collapse Attrs
7 | @interface ICollapseAttrs
8 |
--------------------------------------------------------------------------------
/src/components/collapse/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Button, Collapse, Card } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/collapse/examples/index.ts';
6 | const SAMPLE_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis interdum ex eu eros dictum.
';
7 |
8 | export class CollapseExample {
9 | private isOpen = false;
10 | private content = [SAMPLE_TEXT];
11 |
12 | public view() {
13 | return m(Example, { center: false, src: EXAMPLE_SRC }, [
14 | m(Button, {
15 | label: this.isOpen ? 'Hide collapse' : 'Show collapse',
16 | intent: 'primary',
17 | onclick: this.toggleOpen,
18 | style: 'margin-right: 10px'
19 | }),
20 |
21 | m(Button, {
22 | label: 'Append content',
23 | onclick: this.appendContent
24 | }),
25 |
26 | m(Collapse, { duration: 200, isOpen: this.isOpen }, [
27 | m(Card, { style: 'margin-top:20px' }, this.content.map((text) => m.trust(text)))
28 | ])
29 | ]);
30 | }
31 |
32 | private appendContent = () => {
33 | this.content.push(SAMPLE_TEXT);
34 | };
35 |
36 | private toggleOpen = () => {
37 | this.isOpen = !this.isOpen;
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/collapse/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-collapse {
4 | height: 0;
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/control-group/control-group.md:
--------------------------------------------------------------------------------
1 | @# Control Group
2 | A control group arranges inputs, buttons, button groups and selects into one unit. Note: Each individual child component must specify its own intent/size.
3 |
4 | @example ControlGroupExample
5 |
6 | @## ControlGroup Attrs
7 | @interface IControlGroupAttrs
8 |
--------------------------------------------------------------------------------
/src/components/control-group/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { ControlGroup, Button, Input, Icon, Icons, Select, Spinner, CustomSelect } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/control-group/examples/index.ts';
6 | const options = ['Option 1', 'Option 2'];
7 |
8 | export class ControlGroupExample {
9 | public view() {
10 | return m(Example, { direction: 'column', src: EXAMPLE_SRC }, [
11 | m(ControlGroup, { style: 'margin-bottom:15px' }, [
12 | m(Input, {
13 | contentLeft: m(Icon, { name: Icons.SEARCH }),
14 | placeholder: 'Input placeholder...'
15 | }),
16 | m(Button, {
17 | iconLeft: Icons.USERS,
18 | label: 'Button'
19 | }),
20 | m(Select, { options })
21 | ]),
22 |
23 | m(ControlGroup, [
24 | m(Button, {
25 | iconLeft: Icons.SETTINGS,
26 | label: 'Action'
27 | }),
28 | m(Input, {
29 | contentLeft: m(Icon, { name: Icons.USER }),
30 | contentRight: m(Spinner, { active: true }),
31 | placeholder: 'Enter name...'
32 | }),
33 | m(CustomSelect, {
34 | options,
35 | defaultValue: 'Option 2'
36 | })
37 | ])
38 | ]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/control-group/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-control-group {
4 | display: inline-flex;
5 | vertical-align: middle;
6 | margin-right: $cui-base-size;
7 |
8 | &:last-child {
9 | margin:0;
10 | }
11 |
12 | > :first-child {
13 | border-radius: $cui-border-radius 0 0 $cui-border-radius;
14 | margin-right: -1px;
15 | }
16 |
17 | > :last-child {
18 | border-radius: 0 $cui-border-radius $cui-border-radius 0;
19 | }
20 |
21 | > :only-child {
22 | border-radius: $cui-border-radius;
23 | margin:0;
24 | }
25 |
26 | > :not(:first-child):not(:last-child) {
27 | border-radius: 0;
28 | margin-right: -1px;
29 | }
30 |
31 | .cui-select select,
32 | .cui-input input,
33 | .cui-custom-select button {
34 | border-radius: inherit;
35 | }
36 |
37 | & :focus,
38 | & :hover,
39 | & :active,
40 | & .cui-active {
41 | z-index: 1;
42 | }
43 |
44 | & :focus {
45 | z-index: 1;
46 | }
47 |
48 | &.cui-fluid > * {
49 | flex: 1 1 auto;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/control-group/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, beforeEach, expect, it } from 'vitest';
3 | import { ControlGroup, Classes, IControlGroupAttrs, Button, Input } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('control-group', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | beforeEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | style: 'margin: 0'
14 | });
15 |
16 | expect(hasClass(el(), Classes.CONTROL_GROUP)).toBeTruthy();
17 | expect(el().hasAttribute('style')).toBeTruthy();
18 | });
19 |
20 | it('Renders children', () => {
21 | mount({});
22 |
23 | const length = el().children.length;
24 | expect(length).toBe(2);
25 | });
26 |
27 | it('Passes through html attrs', () => {
28 | mount({
29 | id: 1,
30 | name: 'name'
31 | });
32 |
33 | expect(el().hasAttribute('id')).toBeTruthy();
34 | expect(el().hasAttribute('name')).toBeTruthy();
35 | });
36 |
37 | function mount(attrs: IControlGroupAttrs) {
38 | const component = {
39 | view: () => m(ControlGroup, { ...attrs }, [
40 | m(Button, { label: 'label' }),
41 | m(Input)
42 | ])
43 | };
44 | m.mount(document.body, component);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/control-group/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs } from '../../_shared';
4 |
5 | export interface IControlGroupAttrs extends IAttrs {
6 | [htmlAttrs: string]: any;
7 | }
8 |
9 | export class ControlGroup implements m.Component {
10 | public view({ attrs, children }: m.Vnode) {
11 | const { class: className, ...htmlAttrs } = attrs;
12 |
13 | return m('', {
14 | ...htmlAttrs,
15 | class: classnames(
16 | Classes.CONTROL_GROUP,
17 | className
18 | )
19 | }, children);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/custom-select/custom-select.md:
--------------------------------------------------------------------------------
1 | @# Custom Select
2 | A custom select is composed of a `SelectList` and `Button` component. Functionality is similar to a native select with a few notable changes:
3 | + Navigating using the arrows keys will circle back to start/finish when the end items are reached
4 | + Doesn't implement option keyword selection
5 |
6 | @example CustomSelectExample
7 |
8 | @## Custom item render
9 | @example CustomSelectRenderExample
10 |
11 | @## CustomSelect Attrs
12 | @interface ICustomSelectAttrs
13 |
--------------------------------------------------------------------------------
/src/components/custom-select/examples/default.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Example, SizeSelect } from '@shared/examples';
3 | import { CustomSelect, Size } from '@/';
4 |
5 | const EXAMPLE_SRC = 'components/custom-select/examples/index.ts';
6 | const options = [
7 | {
8 | label: 'First',
9 | value: '1'
10 | },
11 | {
12 | label: 'Second',
13 | value: '2',
14 | disabled: true
15 | },
16 | {
17 | label: 'Third',
18 | value: '3'
19 | },
20 | {
21 | label: 'Fourth',
22 | value: '4'
23 | }
24 | ];
25 |
26 | export class CustomSelectExample {
27 | private size: Size;
28 |
29 | public view() {
30 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
31 | m(CustomSelect, {
32 | defaultValue: '3',
33 | options,
34 | size: this.size
35 | })
36 | ]);
37 | }
38 |
39 | private renderOptions() {
40 | return [
41 | m('h5', 'Size'),
42 | m(SizeSelect, { onSelect: (size: Size) => this.size = size })
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/custom-select/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default';
2 | export * from './itemRender';
3 |
--------------------------------------------------------------------------------
/src/components/custom-select/examples/itemRender.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Example } from '@shared/examples';
3 | import { CustomSelect, ListItem, Icon, Icons, IOption } from '@/';
4 |
5 | const EXAMPLE_SRC = 'components/custom-select/examples/itemRender.ts';
6 | const options = [
7 | {
8 | label: 'First',
9 | value: '1'
10 | },
11 | {
12 | label: 'Second',
13 | value: '2',
14 | disabled: true
15 | },
16 | {
17 | label: 'Third',
18 | value: '3'
19 | },
20 | {
21 | label: 'Fourth',
22 | value: '4'
23 | }
24 | ];
25 |
26 | export class CustomSelectRenderExample {
27 | public view() {
28 | return m(Example, { src: EXAMPLE_SRC }, [
29 | m(CustomSelect, {
30 | triggerAttrs: {
31 | align: 'left',
32 | style: 'width: 300px'
33 | },
34 | defaultValue: '3',
35 | itemRender: (item, isSelected, index) => m(ListItem, {
36 | contentLeft: m(Icon, {
37 | name: index % 2 ? Icons.FILE_PLUS : Icons.USERS
38 | }),
39 | style: index % 2 ? 'color: red' : undefined,
40 | label: (item as IOption).label,
41 | selected: isSelected
42 | }),
43 | options
44 | })
45 | ]);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/custom-select/index.scss:
--------------------------------------------------------------------------------
1 | .cui-custom-select {
2 | display: inline-block;
3 |
4 | .cui-overlay {
5 | position: relative;
6 | }
7 |
8 | .cui-popover {
9 | width: 100%;
10 | transform: none !important;
11 | }
12 |
13 | .cui-popover-content {
14 | width: 100%;
15 | border-top: none;
16 | border-top-left-radius: 0;
17 | border-top-right-radius: 0;
18 | padding: 0;
19 | }
20 | }
21 |
22 |
23 | .cui-custom-select-trigger.cui-active {
24 | border-bottom-left-radius: 0;
25 | border-bottom-right-radius: 0;
26 | }
27 |
28 | .cui-custom-select-input {
29 | display: none;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/dialog/dialog.md:
--------------------------------------------------------------------------------
1 | @# Dialog
2 | A dialog is composed of an `Overlay` component and provides a centered container that has a header, body and footer. It is a controlled component and can only be toggled via the `isOpen` attribute.
3 |
4 | @example DialogExample
5 |
6 | @## Dialog Attrs
7 | @interface IDialogAttrs
8 |
--------------------------------------------------------------------------------
/src/components/drawer/drawer.md:
--------------------------------------------------------------------------------
1 | @# Drawer
2 | A drawer is composed of an `Overlay` component and provides a slide-out side panel. Like the `Dialog` and `Overlay`, a drawer is a controlled component and can only be toggled via the `isOpen` attribute.
3 |
4 | @example DrawerExample
5 |
6 | @## Drawer Attrs
7 | @interface IDrawerAttrs
8 |
--------------------------------------------------------------------------------
/src/components/drawer/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Drawer, IDrawerAttrs, Classes } from '@/';
4 | import { hasChildClass, hasClass } from '@test-utils';
5 |
6 | describe('drawer', () => {
7 | const drawer = () => document.body.querySelector(`.${Classes.DRAWER}`) as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | style: 'color: red',
15 | position: 'right'
16 | });
17 |
18 | expect(hasClass(drawer(), Classes.DRAWER)).toBeTruthy();
19 | expect(hasClass(drawer(), `${Classes.DRAWER}-right`)).toBeTruthy();
20 | });
21 |
22 | it('Renders children', () => {
23 | mount({ content: 'content' });
24 |
25 | expect(hasChildClass(drawer(), Classes.DRAWER_CONTENT)).toBeTruthy();
26 | });
27 |
28 | it('Sets correct position class', () => {
29 | const position = 'top';
30 | mount({ position });
31 |
32 | expect(hasClass(drawer(), `${Classes.DRAWER}-${position}`)).toBeTruthy();
33 | });
34 |
35 | function mount(attrs: IDrawerAttrs) {
36 | const component = {
37 | view: () => m(Drawer, {
38 | isOpen: true,
39 | transitionDuration: 0,
40 | ...attrs
41 | })
42 | };
43 |
44 | m.mount(document.body, component);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/drawer/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes } from '../../_shared';
4 | import { IOverlayAttrs, Overlay } from '../overlay';
5 |
6 | export const DrawerPosition = {
7 | TOP: 'top',
8 | BOTTOM: 'bottom',
9 | RIGHT: 'right',
10 | LEFT: 'left'
11 | } as const;
12 |
13 | export type DrawerPosition = typeof DrawerPosition[keyof typeof DrawerPosition];
14 |
15 | export interface IDrawerAttrs extends IOverlayAttrs {
16 | /** Position of drawer */
17 | position?: DrawerPosition;
18 | }
19 |
20 | export class Drawer implements m.Component {
21 | public view({ attrs }: m.Vnode) {
22 | const { position, content, class: className, style, ...otherAttrs } = attrs;
23 |
24 | const innerContent = m(`.${Classes.DRAWER_CONTENT}`, content);
25 |
26 | const classes = classnames(
27 | Classes.DRAWER,
28 | `${Classes.DRAWER}-${position}`,
29 | className
30 | );
31 |
32 | const container = m('', { class: classes, style }, innerContent);
33 |
34 | return m(Overlay, {
35 | ...otherAttrs as IOverlayAttrs,
36 | content: container
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/empty-state/empty-state.md:
--------------------------------------------------------------------------------
1 | @# Empty State
2 | An empty state component indicates a non-ideal state.
3 |
4 | @example EmptyStateExample
5 |
6 | @## EmptyState Attrs
7 | @interface IEmptyStateAttrs
8 |
--------------------------------------------------------------------------------
/src/components/empty-state/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { EmptyState, Icons, Icon, Input, Switch } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/empty-state/examples/index.ts';
6 |
7 | export class EmptyStateExample {
8 | private hasIcon = true;
9 | private hasHeader = true;
10 | private hasContent = true;
11 | private fill = true;
12 |
13 | public view() {
14 | return m(Example, { options: this.renderOptions(), center: false, src: EXAMPLE_SRC }, [
15 | m(EmptyState, {
16 | icon: this.hasIcon ? Icons.ARCHIVE : undefined,
17 | header: this.hasHeader && 'No search results found.',
18 | content: this.hasContent && m(Input, {
19 | contentLeft: m(Icon, { name: Icons.SEARCH }),
20 | placeholder: 'Search results...'
21 | }),
22 | fill: this.fill
23 | })
24 | ]);
25 | }
26 |
27 | private renderOptions() {
28 | return [
29 | m(Switch, {
30 | checked: this.hasIcon,
31 | label: 'Icon',
32 | onchange: () => this.hasIcon = !this.hasIcon
33 | }),
34 |
35 | m(Switch, {
36 | checked: this.hasHeader,
37 | label: 'Header',
38 | onchange: () => this.hasHeader = !this.hasHeader
39 | }),
40 |
41 | m(Switch, {
42 | checked: this.hasContent,
43 | label: 'Content',
44 | onchange: () => this.hasContent = !this.hasContent
45 | }),
46 |
47 | m(Switch, {
48 | checked: this.fill,
49 | label: 'Fill',
50 | onchange: () => this.fill = !this.fill
51 | })
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/empty-state/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-empty-state {
4 | position: relative;
5 | display: inline-flex;
6 | vertical-align: middle;
7 | align-items: center;
8 | justify-content: center;
9 | flex-direction: column;
10 |
11 | &.cui-empty-state-fill {
12 | position:absolute;
13 | display:flex;
14 | top:0;
15 | left:0;
16 | height:100%;
17 | width:100%;
18 | z-index: $cui-z-index-overlay;
19 | }
20 | }
21 |
22 | .cui-empty-state-icon .cui-icon svg {
23 | height: 40px;
24 | width: 40px;
25 | min-height: 40px;
26 | min-width: 40px;
27 | margin-bottom: 15px;
28 | }
29 |
30 | .cui-empty-state-header {
31 | font-weight: bold;
32 | font-size: $cui-body-font-size;
33 |
34 | &:not(:last-child) {
35 | margin-bottom: 10px;
36 | }
37 | }
38 |
39 | .cui-empty-state-content {
40 | text-align: center;
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/empty-state/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { EmptyState, IEmptyStateAttrs, Classes, Icons } from '@/';
4 | import { hasChildClass, hasClass } from '@test-utils';
5 |
6 | describe('empty-state', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | fill: true,
15 | style: 'color: red'
16 | });
17 |
18 | expect(hasClass(el(), Classes.EMPTY_STATE)).toBeTruthy();
19 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
20 | expect(hasClass(el(), Classes.EMPTY_STATE_FILL)).toBeTruthy();
21 | expect(el().hasAttribute('style')).toBeTruthy();
22 | });
23 |
24 | it('Renders children', () => {
25 | mount({
26 | content: 'content',
27 | header: 'header',
28 | icon: Icons.ACTIVITY
29 | });
30 |
31 | expect(hasChildClass(el(), Classes.EMPTY_STATE_CONTENT)).toBeTruthy();
32 | expect(hasChildClass(el(), Classes.EMPTY_STATE_HEADER)).toBeTruthy();
33 | expect(hasChildClass(el(), Classes.ICON)).toBeTruthy();
34 | });
35 |
36 | it('Passes through html attrs', () => {
37 | mount({
38 | id: 1,
39 | name: 'name'
40 | });
41 |
42 | expect(el().hasAttribute('id')).toBeTruthy();
43 | expect(el().hasAttribute('name')).toBeTruthy();
44 | });
45 |
46 | function mount(attrs: IEmptyStateAttrs) {
47 | const component = {
48 | view: () => m(EmptyState, { ...attrs })
49 | };
50 |
51 | m.mount(document.body, component);
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/empty-state/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs } from '../../_shared';
4 | import { Icon, IconName } from '../icon';
5 |
6 | export interface IEmptyStateAttrs extends IAttrs {
7 | /** Icon name */
8 | icon?: IconName | m.Children;
9 |
10 | /** Header content */
11 | header?: m.Children;
12 |
13 | /** Main content */
14 | content?: m.Children;
15 |
16 | /**
17 | * Fills the height/width of parent container
18 | * @default true
19 | */
20 | fill?: boolean;
21 |
22 | [htmlAttrs: string]: any;
23 | }
24 |
25 | export class EmptyState implements m.Component {
26 | public view({ attrs }: m.Vnode) {
27 | const { class: className, fill = true, icon, header, content, ...htmlAttrs } = attrs;
28 |
29 | const classes = classnames(
30 | Classes.EMPTY_STATE,
31 | fill && Classes.EMPTY_STATE_FILL,
32 | className
33 | );
34 |
35 | const container = [
36 | icon && m(`.${Classes.EMPTY_STATE_ICON}`, [
37 | typeof icon === 'string'
38 | ? m(Icon, { name: icon as IconName })
39 | : icon
40 | ]),
41 | header && m(`.${Classes.EMPTY_STATE_HEADER}`, header),
42 | content && m(`.${Classes.EMPTY_STATE_CONTENT}`, content)
43 | ];
44 |
45 | return m('', { ...htmlAttrs, class: classes }, container);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/form/Form.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs } from '../../_shared';
4 | import { Grid, IGridAttrs } from '../grid';
5 |
6 | export interface IFormAttrs extends IAttrs, IGridAttrs {
7 | [htmlAttrs: string]: any;
8 | }
9 |
10 | export class Form implements m.Component {
11 | public view({ attrs, children }: m.Vnode) {
12 | const classes = classnames(
13 | Classes.FORM,
14 | attrs.class
15 | );
16 |
17 | return m(Grid, {
18 | ...attrs,
19 | element: 'form',
20 | class: classes
21 | }, children);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/form/FormGroup.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes } from '../../_shared';
4 | import { FormLabel } from './FormLabel';
5 | import { IColAttrs, Col } from '../grid';
6 |
7 | export interface IFormGroupAttrs extends IColAttrs {
8 | /** Text label */
9 | label?: string;
10 |
11 | /** Inner content; can be used instead of passing children */
12 | content?: m.Children;
13 |
14 | /** Disables interaction */
15 | disabled?: boolean;
16 | }
17 |
18 | export class FormGroup implements m.Component {
19 | public view({ attrs, children }: m.Vnode) {
20 | const {
21 | class: className,
22 | content,
23 | disabled,
24 | label,
25 | span = 12,
26 | ...htmlAttrs
27 | } = attrs;
28 |
29 | const classes = classnames(
30 | Classes.FORM_GROUP,
31 | disabled && Classes.DISABLED,
32 | className
33 | );
34 |
35 | const innerContent = [
36 | label && m(FormLabel, label),
37 | content || children
38 | ];
39 |
40 | return m(Col, {
41 | class: classes,
42 | span,
43 | ...htmlAttrs
44 | }, innerContent);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/form/FormLabel.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Classes, IAttrs } from '../../_shared';
3 |
4 | export interface IFormLabelAttrs extends IAttrs {
5 | [htmlAttrs: string]: any;
6 | }
7 |
8 | export class FormLabel implements m.Component {
9 | public view({ attrs, children }: m.Vnode) {
10 | return m(`label.${Classes.FORM_LABEL}`, { ...attrs }, children);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/form/form.md:
--------------------------------------------------------------------------------
1 | @# Form
2 | The form component provides a simple wrapper over the HTML `form` element and arranges an array of `FormGroup` components. The form is wrapped in a `Grid` component and each `FormGroup` is wrapped in a `Col` component allowing for responsive styling.
3 |
4 | @example FormExample
5 |
6 | @## Form Attrs
7 | @interface IFormAttrs
8 |
9 | @## FormGroup Attrs
10 | @interface IFormGroupAttrs
11 |
12 | @## FormLabel Attrs
13 | @interface IAttrs
14 |
--------------------------------------------------------------------------------
/src/components/form/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-form {
4 | .cui-form-group {
5 | margin-bottom: $cui-base-size * 1.5;
6 |
7 | .cui-input,
8 | .cui-input-file,
9 | .cui-custom-select {
10 | width:100%;
11 | }
12 |
13 | &:last-child {
14 | margin:0;
15 | }
16 |
17 | &.cui-disabled {
18 | opacity: 0.75;
19 | pointer-events: none;
20 | }
21 | }
22 |
23 | .cui-form-label {
24 | font-size: 13px;
25 | font-weight:bold;
26 | display:inline-block;
27 | margin-bottom: $cui-base-size;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/form/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Form, IFormAttrs, Classes } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | // TODO: add FormGroup tests
7 | describe('form', () => {
8 | const el = () => document.body.firstChild as HTMLElement;
9 |
10 | afterEach(() => m.mount(document.body, null));
11 |
12 | it('Renders correctly', () => {
13 | mount({
14 | class: Classes.FORM,
15 | style: 'color: red'
16 | });
17 |
18 | expect(hasClass(el(), Classes.FORM)).toBeTruthy();
19 | expect(el().tagName).toBe('FORM');
20 | expect(el().style.color).toBe('red');
21 | });
22 |
23 | function mount(attrs: IFormAttrs) {
24 | const component = {
25 | view: () => m(Form, { ...attrs })
26 | };
27 |
28 | m.mount(document.body, component);
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/form/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Form';
2 | export * from './FormGroup';
3 | export * from './FormLabel';
4 |
--------------------------------------------------------------------------------
/src/components/grid/Col.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, Breakpoints, getObjectKeys } from '../../_shared';
4 | import { IBreakpointMap } from './Grid';
5 |
6 | export interface IColAttrs extends IAttrs {
7 | /** Width of column; between 1-12 */
8 | span?: number | IBreakpointMap;
9 |
10 | /** Column order */
11 | order?: number | IBreakpointMap;
12 |
13 | /** Column offset */
14 | offset?: number | IBreakpointMap;
15 |
16 | [htmlAttrs: string]: any;
17 | }
18 |
19 | export class Col implements m.Component {
20 | public view({ attrs, children }: m.Vnode) {
21 | const { span, order, offset, class: className, ...htmlAttrs } = attrs;
22 |
23 | let breakpointClasses: string = '';
24 |
25 | getObjectKeys(Breakpoints).map(breakpoint => {
26 | breakpointClasses = classnames(
27 | breakpointClasses,
28 | typeof span === 'object' && span[breakpoint] && `${Classes.COL}-${breakpoint}-${span[breakpoint]}`,
29 | typeof order === 'object' && order[breakpoint] && `${Classes.COL}-${breakpoint}-order-${order[breakpoint]}`,
30 | typeof offset === 'object' && offset[breakpoint] && `${Classes.COL}-${breakpoint}-offset-${offset[breakpoint]}`
31 | );
32 | });
33 |
34 | const classes = classnames(
35 | breakpointClasses,
36 | typeof span === 'number' && `${Classes.COL}-${span}`,
37 | typeof order === 'number' && `${Classes.COL}-order-${order}`,
38 | typeof offset === 'number' && `${Classes.COL}-offset-${offset}`,
39 | className
40 | );
41 |
42 | return m('', { ...htmlAttrs, class: classes }, children);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/grid/examples/basic.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Col, Grid, Switch, Select } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/grid/examples/basic.ts';
6 |
7 | export class GridBasicExample {
8 | private gutter = false;
9 | private align: any;
10 | private justify: any;
11 |
12 | public view() {
13 | const gridAttrs = {
14 | gutter: this.gutter ? 20 : 0,
15 | align: this.align,
16 | justify: this.justify
17 | };
18 |
19 | const exampleAttrs = {
20 | class: 'cui-example-grid',
21 | options: this.renderOptions(),
22 | center: false,
23 | src: EXAMPLE_SRC
24 | };
25 |
26 | return m(Example, exampleAttrs, [
27 | m(Grid, { ...gridAttrs }, [
28 | m(Col, { span: 8 }, m('.cui-example-grid-col', 'col-8')),
29 | m(Col, { span: 4 }, m('.cui-example-grid-col', 'col-4'))
30 | ]),
31 |
32 | m(Grid, { ...gridAttrs }, [
33 | m(Col, { span: 4 }, m('.cui-example-grid-col', 'col-4')),
34 | m(Col, { span: 4 }, m('.cui-example-grid-col[style=height:100px]', 'col-4')),
35 | m(Col, { span: 3 }, m('.cui-example-grid-col', 'col-3'))
36 | ])
37 | ]);
38 | }
39 |
40 | private renderOptions() {
41 | return [
42 | m('h5', 'Align'),
43 | m(Select, {
44 | options: ['top', 'middle', 'bottom'],
45 | onchange: (e: Event) => this.align = (e.target as HTMLInputElement).value,
46 | size: 'xs'
47 | }),
48 | m('h5', 'Justify'),
49 | m(Select, {
50 | options: ['start', 'center', 'space-around', 'space-between'],
51 | onchange: (e: Event) => this.justify = (e.target as HTMLInputElement).value,
52 | size: 'xs'
53 | }),
54 | m(Switch, {
55 | checked: this.gutter,
56 | label: 'Gutter',
57 | onchange: () => this.gutter = !this.gutter
58 | })
59 | ];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/grid/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './basic';
2 | export * from './offset';
3 | export * from './order';
4 | export * from './responsive';
5 |
--------------------------------------------------------------------------------
/src/components/grid/examples/offset.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Col, Grid } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/grid/examples/offset.ts';
6 |
7 | export class GridOffsetExample {
8 | public view() {
9 | return m(Example, { class: 'cui-example-grid', center: false, src: EXAMPLE_SRC }, [
10 | m(Grid, m(Col, { span: 2, offset: 10 }, m('.cui-example-grid-col', 'offset-10'))),
11 | m(Grid, m(Col, { span: 3, offset: 9 }, m('.cui-example-grid-col', 'offset-9'))),
12 | m(Grid, m(Col, { span: 4, offset: 8 }, m('.cui-example-grid-col', 'offset-8'))),
13 | m(Grid, m(Col, { span: 5, offset: 7 }, m('.cui-example-grid-col', 'offset-7'))),
14 | m(Grid, m(Col, { span: 6, offset: 6 }, m('.cui-example-grid-col', 'offset-6')))
15 | ]);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/grid/examples/order.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Col, Grid } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/grid/examples/order.ts';
6 |
7 | export class GridOrderExample {
8 | public view() {
9 | return m(Example, { class: 'cui-example-grid', center: false, src: EXAMPLE_SRC }, [
10 | m(Grid, [
11 | m(Col, { span: 4, order: 3 }, m('.cui-example-grid-col', 'first')),
12 | m(Col, { span: 4, order: 1 }, m('.cui-example-grid-col', 'second')),
13 | m(Col, { span: 4, order: 2 }, m('.cui-example-grid-col', 'third'))
14 | ])
15 | ]);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/grid/examples/responsive.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Col, Grid, Table, Breakpoints, getObjectKeys } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/grid/examples/responsive.ts';
6 | const breakpoints = getObjectKeys(Breakpoints);
7 |
8 | export class GridResponsiveExample {
9 | public view() {
10 | return [
11 | m(Example, { class: 'cui-example-grid', center: false, src: EXAMPLE_SRC }, [
12 | m(Grid, { gutter: { xs: 0, sm: 10, md: 20, lg: 30, xl: 40 } }, [
13 | m(Col, { span: { xs: 12, md: 4 } }, m('.cui-example-grid-col', 'col')),
14 | m(Col, { span: { xs: 12, md: 4 } }, m('.cui-example-grid-col', 'col')),
15 | m(Col, { span: { xs: 12, md: 4 } }, m('.cui-example-grid-col', 'col'))
16 | ])
17 | ]),
18 |
19 | m(Table, [
20 | m('tr', [
21 | m('th', 'Size'),
22 | m('th', 'Description')
23 | ]),
24 | breakpoints.map(breakpoint => m('tr', [
25 | m('td', m('code', breakpoint)),
26 | m('td', Breakpoints[breakpoint])
27 | ]))
28 | ])
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/grid/grid.md:
--------------------------------------------------------------------------------
1 | @# Grid
2 | A grid allows for a responsive column layout.
3 |
4 | @example GridBasicExample
5 |
6 | @## Column Offset
7 | Individual columns can be offset.
8 | @example GridOffsetExample
9 |
10 | @## Column Order
11 | Individual columns can be reordered.
12 | @example GridOrderExample
13 |
14 | @## Responsive
15 | A grid allows for responsive layouts.
16 | @example GridResponsiveExample
17 |
18 | @## Grid Attrs
19 | @interface IGridAttrs
20 |
21 | @## Col Attrs
22 | @interface IColAttrs
23 |
--------------------------------------------------------------------------------
/src/components/grid/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Grid';
2 | export * from './Col';
3 |
--------------------------------------------------------------------------------
/src/components/icon/Icon.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 | import { IconContents, Icons } from './generated';
5 |
6 | export type IconName = (typeof Icons)[keyof typeof Icons];
7 |
8 | export interface IIconAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
9 | /** Icon name */
10 | name: IconName;
11 |
12 | /** Callback invoked on click; Passing this attr will apply hover styles to the icon */
13 | onclick?: (e: Event) => void;
14 |
15 | [htmlAttrs: string]: any;
16 | }
17 |
18 | export class Icon implements m.Component {
19 | public view({ attrs }: m.Vnode) {
20 | const { class: className, intent, name, onclick, size, ...htmlAttrs } = attrs;
21 |
22 | const classes = classnames(
23 | Classes.ICON,
24 | `${Classes.ICON}-${name}`,
25 | intent && `cui-${intent}`,
26 | size && `cui-${size}`,
27 | onclick && Classes.ICON_ACTION,
28 | className
29 | );
30 |
31 | const svg = m.trust(``);
32 |
33 | return m('', {
34 | ...htmlAttrs,
35 | class: classes,
36 | onclick
37 | }, svg);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/icon/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Switch, Icon, Intent, Icons, IconName, SelectList, ListItem, Button, getObjectKeys } from '@/';
3 | import { Example, IntentSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/icon/examples/index.ts';
6 | const iconNames = getObjectKeys(Icons).slice(1);
7 | const icons = Icons as any;
8 |
9 | export class IconExample {
10 | private intent: Intent;
11 | private interactive = false;
12 | private iconName: IconName = Icons.SETTINGS;
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
16 | m(Icon, {
17 | intent: this.intent,
18 | name: this.iconName,
19 | onclick: this.interactive ? () => null : undefined,
20 | size: 'xl'
21 | })
22 | ]);
23 | }
24 |
25 | private renderOptions() {
26 | return [
27 | m('h5', 'Intent'),
28 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
29 | m('h5', 'Icon name'),
30 | m(SelectList, {
31 | closeOnSelect: false,
32 | items: iconNames,
33 | itemRender: (iconName: IconName) => m(ListItem, {
34 | contentLeft: m(Icon, { name: icons[iconName] }),
35 | label: iconName,
36 | selected: this.iconName === icons[iconName]
37 | }),
38 | itemPredicate: (query: string, item: string) => {
39 | return item.toLowerCase().includes(query.toLowerCase());
40 | },
41 | trigger: m(Button, {
42 | align: 'left',
43 | compact: true,
44 | iconRight: Icons.CHEVRON_DOWN,
45 | label: this.iconName,
46 | size: 'xs',
47 | style: 'margin-bottom: 10px',
48 | fluid: true
49 | }),
50 | onSelect: (iconName: IconName) => this.iconName = icons[iconName]
51 | }),
52 |
53 | m(Switch, {
54 | checked: this.interactive,
55 | label: 'Interactive',
56 | onchange: () => this.interactive = !this.interactive
57 | })
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/icon/icon.md:
--------------------------------------------------------------------------------
1 | @# Icon
2 | An SVG icon.
3 |
4 | @example IconExample
5 |
6 | @## Icon Attrs
7 | @interface IIconAttrs
8 |
--------------------------------------------------------------------------------
/src/components/icon/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | $cui-icon-base: 16px;
4 | $cui-icon-xs: 14px;
5 | $cui-icon-sm: 14px;
6 | $cui-icon-lg: 18px;
7 | $cui-icon-xl: 20px;
8 |
9 | $cui-icon-size-map: (
10 | xs: $cui-icon-xs,
11 | sm: $cui-icon-sm,
12 | lg: $cui-icon-lg,
13 | xl: $cui-icon-xl
14 | );
15 |
16 | @mixin cui-icon-sizing($size) {
17 | svg {
18 | height: $size;
19 | width: $size;
20 | min-width: $size;
21 | min-height: $size;
22 | }
23 | }
24 |
25 | @mixin cui-icon-style($intent) {
26 | color: $intent;
27 |
28 | &.cui-icon-action:hover {
29 | color: shade($intent, 10%);
30 | }
31 | }
32 |
33 | .cui-icon {
34 | position: relative;
35 | display: inline-flex;
36 | vertical-align: middle;
37 | color: $cui-icon-color;
38 | @include cui-icon-sizing($cui-icon-base);
39 |
40 | svg {
41 | display:block;
42 | stroke: currentColor;
43 | stroke-width: 2px;
44 | stroke-linecap: round;
45 | stroke-linejoin: round;
46 | fill: none;
47 | }
48 |
49 | &.cui-icon-action:hover {
50 | cursor: pointer;
51 | color: $cui-icon-color-hover;
52 | }
53 |
54 | @each $size in $cui-sizes {
55 | &.cui-#{$size},
56 | .cui-#{$size} & {
57 | @include cui-icon-sizing(map-get($cui-icon-size-map, $size));
58 | }
59 | }
60 |
61 | @each $intent in $cui-intents {
62 | &.cui-#{$intent},
63 | .cui-#{$intent} > & {
64 | @include cui-icon-style(map-get($cui-bg-color-map, $intent));
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/icon/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Icon, IIconAttrs, Classes, Icons } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('icon', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | name: Icons.ACTIVITY,
14 | class: Classes.POSITIVE,
15 | intent: 'primary',
16 | style: 'color: red'
17 | });
18 |
19 | expect(hasClass(el(), Classes.ICON)).toBeTruthy();
20 | expect(hasClass(el(), `${Classes.ICON}-${Icons.ACTIVITY}`)).toBeTruthy();
21 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
22 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
23 | expect(el().hasAttribute('style')).toBeTruthy();
24 | });
25 |
26 | it('Passes through html attrs', () => {
27 | mount({
28 | id: 1,
29 | name: Icons.ACTIVITY
30 | });
31 |
32 | expect(el().hasAttribute('id')).toBeTruthy();
33 | });
34 |
35 | it('Passing onclick sets interactive class', () => {
36 | mount({
37 | onclick: () => null,
38 | name: Icons.ACTIVITY
39 | });
40 |
41 | expect(hasClass(el(), Classes.ICON_ACTION)).toBeTruthy();
42 | });
43 |
44 | function mount(attrs: IIconAttrs) {
45 | const component = {
46 | view: () => m(Icon, { ...attrs })
47 | };
48 | m.mount(document.body, component);
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/icon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Icon';
2 | export { Icons } from './generated';
3 |
--------------------------------------------------------------------------------
/src/components/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | reference: components
3 | title: 'Components'
4 | ---
5 |
6 | @page breadcrumb
7 | @page button
8 | @page button-group
9 | @page callout
10 | @page card
11 | @page checkbox
12 | @page collapse
13 | @page control-group
14 | @page custom-select
15 | @page dialog
16 | @page drawer
17 | @page empty-state
18 | @page form
19 | @page grid
20 | @page icon
21 | @page input
22 | @page input-file
23 | @page input-popover
24 | @page input-select
25 | @page list
26 | @page menu
27 | @page overlay
28 | @page popover
29 | @page popover-menu
30 | @page portal
31 | @page query-list
32 | @page radio
33 | @page select
34 | @page select-list
35 | @page spinner
36 | @page switch
37 | @page tabs
38 | @page table
39 | @page tag
40 | @page tag-input
41 | @page text-area
42 | @page toast
43 | @page tooltip
44 | @page tree
45 |
--------------------------------------------------------------------------------
/src/components/index.scss:
--------------------------------------------------------------------------------
1 | @import '../_shared/variables';
2 | @import '../_shared/utility';
3 | @import '../_shared/colors';
4 |
5 | @import './base-control/index';
6 | @import './breadcrumb/index';
7 | @import './button/index';
8 | @import './button-group/index';
9 | @import './card/index';
10 | @import './callout/index';
11 | @import './checkbox/index';
12 | @import './collapse/index';
13 | @import './control-group/index';
14 | @import './custom-select/index';
15 | @import './dialog/index';
16 | @import './drawer/index';
17 | @import './empty-state/index';
18 | @import './form/index';
19 | @import './grid/index';
20 | @import './icon/index';
21 | @import './input/index';
22 | @import './input-file/index';
23 | @import './input-popover/index';
24 | @import './list/index';
25 | @import './menu/index';
26 | @import './overlay/index';
27 | @import './popover/index';
28 | @import './popover-menu/index';
29 | @import './portal/index';
30 | @import './query-list/index';
31 | @import './radio/index';
32 | @import './select/index';
33 | @import './spinner/index';
34 | @import './switch/index';
35 | @import './tabs/index';
36 | @import './table/index';
37 | @import './tag/index';
38 | @import './tag-input/index';
39 | @import './text-area/index';
40 | @import './toast/index';
41 | @import './tooltip/index';
42 | @import './tree/index';
43 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './breadcrumb';
2 | export * from './button';
3 | export * from './button-group';
4 | export * from './card';
5 | export * from './callout';
6 | export * from './checkbox';
7 | export * from './collapse';
8 | export * from './control-group';
9 | export * from './custom-select';
10 | export * from './dialog';
11 | export * from './drawer';
12 | export * from './empty-state';
13 | export * from './form';
14 | export * from './grid';
15 | export * from './icon';
16 | export * from './input';
17 | export * from './input-file';
18 | export * from './input-popover';
19 | export * from './input-select';
20 | export * from './list';
21 | export * from './menu';
22 | export * from './overlay';
23 | export * from './popover';
24 | export * from './popover-menu';
25 | export * from './portal';
26 | export * from './query-list';
27 | export * from './radio';
28 | export * from './select';
29 | export * from './select-list';
30 | export * from './spinner';
31 | export * from './switch';
32 | export * from './table';
33 | export * from './tabs';
34 | export * from './tag';
35 | export * from './tag-input';
36 | export * from './text-area';
37 | export * from './toast';
38 | export * from './tooltip';
39 | export * from './tree';
40 |
--------------------------------------------------------------------------------
/src/components/input-file/input-file.md:
--------------------------------------------------------------------------------
1 | @# Input File
2 | A user file input.
3 |
4 | @example InputFileExample
5 |
6 | @## InputFile Attrs
7 | @interface IInputFileAttrs
8 |
--------------------------------------------------------------------------------
/src/components/input-popover/_index.scss:
--------------------------------------------------------------------------------
1 | .cui-input-popover {
2 | .cui-input, .cui-textarea {
3 | margin-bottom: 20px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/input-popover/input-popover.md:
--------------------------------------------------------------------------------
1 | @# Input Popover
2 | An input popover is composed of a `Popover` and `input` or `textarea` element along with a submit `Button`.
3 |
4 | @example InputPopoverExample
5 |
6 | @## Input Popover Attrs
7 | @interface IInputPopoverAttrs
8 |
--------------------------------------------------------------------------------
/src/components/input-select/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { ListItem, InputSelect, Switch } from '@/';
3 | import { Example, countries, ICountryModel } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/input-select/examples/index.ts';
6 | const countryInputSelect = InputSelect.ofType();
7 |
8 | export class InputSelectExample {
9 | private selectedItem: ICountryModel = countries[0];
10 | private closeOnSelect = true;
11 | private openOnDownKey = true;
12 |
13 | public view() {
14 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
15 | m(countryInputSelect, {
16 | closeOnSelect: this.closeOnSelect,
17 | items: countries,
18 | itemRender: this.renderItem,
19 | itemPredicate: this.itemPredicate,
20 | onSelect: this.handleSelect,
21 | popoverAttrs: { hasArrow: false },
22 | value: this.selectedItem && this.selectedItem.name,
23 | openOnDownKey: this.openOnDownKey
24 | })
25 | ]);
26 | }
27 |
28 | private renderItem = (item: ICountryModel) => m(ListItem, {
29 | label: item.name,
30 | selected: this.selectedItem && this.selectedItem.name === item.name
31 | });
32 |
33 | private itemPredicate(query: string, item: ICountryModel) {
34 | return item.name.toLowerCase().includes(query.toLowerCase());
35 | }
36 |
37 | private handleSelect = (item: ICountryModel) => this.selectedItem = item;
38 |
39 | private renderOptions() {
40 | return [
41 | m(Switch, {
42 | checked: this.closeOnSelect,
43 | label: 'Close on select',
44 | onchange: () => this.closeOnSelect = !this.closeOnSelect
45 | }),
46 |
47 | m(Switch, {
48 | checked: this.openOnDownKey,
49 | label: 'Open on down key',
50 | onchange: () => this.openOnDownKey = !this.openOnDownKey
51 | })
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/input-select/input-select.md:
--------------------------------------------------------------------------------
1 | @# Input Select
2 | An input select is composed of a `SelectList` but uses an `Input` as the trigger.
3 |
4 | @example InputSelectExample
5 |
6 | @## Input Select Attrs
7 | @interface IInputSelectAttrs
8 |
--------------------------------------------------------------------------------
/src/components/input/input.md:
--------------------------------------------------------------------------------
1 | @# Input
2 | A user input.
3 |
4 | @example InputExample
5 |
6 | @## Input Attrs
7 | @interface IInputAttrs
8 |
--------------------------------------------------------------------------------
/src/components/list/List.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs } from '../../_shared';
4 |
5 | export interface IListAttrs extends IAttrs, ISizeAttrs {
6 | /** Wether to show background on item hover */
7 | interactive?: boolean;
8 |
9 | [htmlAttrs: string]: any;
10 | }
11 |
12 | export class List implements m.Component {
13 | public view({ attrs, children }: m.Vnode) {
14 | const { class: className, size, interactive = true, ...htmlAttrs } = attrs;
15 |
16 | return m('', {
17 | ...htmlAttrs,
18 | class: classnames(
19 | Classes.LIST,
20 | interactive && Classes.INTERACTIVE,
21 | size && `cui-${size}`,
22 | className
23 | )
24 | }, children);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/list/examples/complex.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Button, Icon, Icons, List, ListItem, PopoverMenu, MenuItem } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/list/examples/complex.ts';
6 |
7 | const data = [
8 | 'List item 1',
9 | 'List item 2',
10 | 'List item 3',
11 | 'List item 4'
12 | ];
13 |
14 | export class ListComplexExample {
15 | public view() {
16 | return m(Example, { src: EXAMPLE_SRC }, [
17 | m(List, data.map(item => m(ListItem, {
18 | contentLeft: m(Icon, { name: Icons.LINK }),
19 | contentRight: m(PopoverMenu, {
20 | closeOnContentClick: true,
21 | content: [
22 | m(MenuItem, {
23 | iconLeft: Icons.EDIT,
24 | label: 'Edit'
25 | }),
26 | m(MenuItem, {
27 | iconLeft: Icons.TRASH_2,
28 | label: 'Delete',
29 | intent: 'negative'
30 | })
31 | ],
32 | trigger: m(Button, {
33 | iconLeft: Icons.MORE_HORIZONTAL,
34 | size: 'xs'
35 | }),
36 | position: 'bottom-end'
37 | }),
38 | label: item
39 | })))
40 | ]);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/list/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './complex';
2 | export * from './nested';
3 | export * from './simple';
4 |
--------------------------------------------------------------------------------
/src/components/list/examples/nested.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { List, ListItem } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/list/examples/nested.ts';
6 |
7 | interface IDataType {
8 | label: string;
9 | children: string[];
10 | }
11 |
12 | const data = [
13 | {
14 | label: 'Header 1',
15 | children: [
16 | 'List item 1',
17 | 'List item 2',
18 | 'List item 3'
19 | ]
20 | },
21 | {
22 | label: 'Header 2',
23 | children: [
24 | 'List item 4',
25 | 'List item 5',
26 | 'List item 6'
27 | ]
28 | },
29 | {
30 | label: 'Header 3',
31 | children: [
32 | 'List item 7',
33 | 'List item 8',
34 | 'List item 9'
35 | ]
36 | }
37 | ] as IDataType[];
38 |
39 | export class ListNestedExample {
40 | public view() {
41 | return m(Example, { src: EXAMPLE_SRC }, [
42 | m(List, data.map(item => m('', [
43 | m('h4', {
44 | style: {
45 | margin: 0,
46 | padding: '20px 0 10px 10px'
47 | }
48 | }, item.label),
49 |
50 | m(List, item.children.map(nestedItem => m(ListItem, { label: nestedItem })))
51 | ])))
52 | ]);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/list/examples/simple.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { List, ListItem, Size, Switch } from '@/';
3 | import { Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/list/examples/simple.ts';
6 |
7 | const data = [
8 | { key: 1, name: 'List item 1' },
9 | { key: 2, name: 'List item 2' },
10 | { key: 3, name: 'List item 3' },
11 | { key: 4, name: 'List item 4' }
12 | ];
13 |
14 | export class ListSimpleExample {
15 | private size: Size;
16 | private interactive: boolean = true;
17 |
18 | public view() {
19 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
20 | m(List, {
21 | interactive: this.interactive,
22 | size: this.size
23 | }, data.map(item => m(ListItem, { label: `List item ${item.key}` })))
24 | ]);
25 | }
26 |
27 | private renderOptions() {
28 | return [
29 | m('h5', 'Size'),
30 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
31 | m(Switch, {
32 | checked: this.interactive,
33 | label: 'Intearctive',
34 | onchange: () => this.interactive = !this.interactive
35 | })
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/list/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @import '../../_shared/variables';
4 |
5 | @mixin cui-list-sizing($padding, $font-size) {
6 | $item-padding: floor(math.div($padding, 1.2));
7 |
8 | .cui-list-item {
9 | padding: $item-padding;
10 | font-size: $font-size;
11 | }
12 |
13 | .cui-list-item-content-left {
14 | padding-right: $item-padding;
15 | }
16 |
17 | .cui-list-item-content-right {
18 | padding-left: $item-padding;
19 | }
20 | }
21 |
22 | .cui-list {
23 | @include cui-list-sizing($cui-base-padding, $cui-font-size);
24 | background:white;
25 | position:relative;
26 | width:100%;
27 | overflow-y:auto;
28 | max-height:400px;
29 |
30 | .cui-list-item:hover {
31 | background: none;
32 | cursor: default;
33 | }
34 |
35 | &.cui-interactive .cui-list-item:hover {
36 | background: $cui-hover-color;
37 | cursor: pointer;
38 | }
39 | }
40 |
41 | .cui-list-item {
42 | position:relative;
43 | display:flex;
44 | align-items: center;
45 | border-bottom: solid 1px $cui-base-border-color;
46 | color: $cui-text-color;
47 | font-weight: normal;
48 | cursor:pointer;
49 |
50 | &:hover,
51 | &.cui-active {
52 | background: $cui-hover-color;
53 | }
54 |
55 | &.cui-selected {
56 | color: $cui-primary-bg-color;
57 | font-weight: bold;
58 | }
59 |
60 | &.cui-disabled {
61 | opacity: 0.5 !important;
62 | &:hover { background: none; }
63 | }
64 |
65 | &:last-child {
66 | border-bottom: none;
67 | }
68 | }
69 |
70 | .cui-list-item-content-left {
71 | .cui-icon {
72 | display:block;
73 | }
74 | }
75 |
76 | .cui-list-item-content-right {
77 | margin-left: auto;
78 | }
79 |
80 | @each $size in $cui-sizes {
81 | .cui-list.cui-#{$size} {
82 | @include cui-list-sizing(
83 | map-get($cui-padding-map, $size),
84 | map-get($cui-font-size-map, $size)
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/list/index.ts:
--------------------------------------------------------------------------------
1 | export * from './List';
2 | export * from './ListItem';
3 |
--------------------------------------------------------------------------------
/src/components/list/list.md:
--------------------------------------------------------------------------------
1 | @# List
2 | A list arranges a group of items in vertical format.
3 |
4 | @example ListSimpleExample
5 |
6 | @## Nested
7 | A list can have a nested list(s) within each item.
8 | @example ListNestedExample
9 |
10 | @## Content on both sides
11 | Each item can have content on either side.
12 | @example ListComplexExample
13 |
14 | @## List Attrs
15 | @interface IListAttrs
16 |
17 | @## ListItem Attrs
18 | @interface IListItemAttrs
19 |
--------------------------------------------------------------------------------
/src/components/menu/Menu.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs } from '../../_shared';
4 |
5 | export interface IMenuAttrs extends IAttrs, ISizeAttrs {
6 | /** Toggles basic styling (no border) */
7 | basic?: boolean;
8 |
9 | [htmlAttrs: string]: any;
10 | }
11 |
12 | export class Menu implements m.Component {
13 | public view({ attrs, children }: m.Vnode) {
14 | const { basic, class: className, size, ...htmlAttrs } = attrs;
15 |
16 | const classes = classnames(
17 | Classes.MENU,
18 | basic && Classes.BASIC,
19 | size && `cui-${size}`,
20 | className
21 | );
22 |
23 | return m('', { ...htmlAttrs, class: classes }, children);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/menu/MenuDivider.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Classes } from '../../_shared';
3 |
4 | export class MenuDivider implements m.Component {
5 | public view() {
6 | return m(`.${Classes.MENU_DIVIDER}`);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/menu/MenuHeading.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs } from '../../_shared';
4 |
5 | export interface IMenuHeadingAttrs extends IAttrs, ISizeAttrs { }
6 |
7 | export class MenuHeading implements m.Component {
8 | public view({ attrs, children }: m.Vnode) {
9 | const { class: className, ...htmlAttrs } = attrs;
10 |
11 | return m('', {
12 | class: classnames(Classes.MENU_HEADING, className),
13 | ...htmlAttrs
14 | }, children);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/menu/MenuItem.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes } from '../../_shared';
4 | import { Button, IButtonAttrs } from '../button';
5 | import { PopoverMenu, IPopoverMenuAttrs } from '../popover-menu';
6 | import { Icons } from '../icon';
7 |
8 | export interface IMenuItemAttrs extends IButtonAttrs {
9 | /** Submenu (Menu component) */
10 | submenu?: m.Children;
11 |
12 | /** Close submenu on child item click */
13 | closeOnSubmenuClick?: boolean;
14 |
15 | /** Attrs passed through to Popover (if submenu exists) */
16 | popoverMenuAttrs?: Partial;
17 |
18 | [htmlAttrs: string]: any;
19 | }
20 |
21 | export class MenuItem implements m.Component {
22 | public view({ attrs }: m.Vnode) {
23 | const {
24 | class: className,
25 | submenu,
26 | closeOnSubmenuClick,
27 | popoverMenuAttrs,
28 | ...buttonAttrs
29 | } = attrs;
30 |
31 | const classes = classnames(
32 | Classes.MENU_ITEM,
33 | Classes.BASIC,
34 | className
35 | );
36 |
37 | const button = m(Button, {
38 | align: 'left',
39 | compact: true,
40 | iconRight: submenu ? Icons.CHEVRON_RIGHT : undefined,
41 | ...buttonAttrs,
42 | class: classes
43 | });
44 |
45 | return submenu ? m(PopoverMenu, {
46 | hasArrow: false,
47 | interactionType: 'hover',
48 | openOnTriggerFocus: true,
49 | position: 'right-start',
50 | ...popoverMenuAttrs,
51 | closeOnContentClick: closeOnSubmenuClick,
52 | addToStack: false,
53 | content: submenu,
54 | inline: true,
55 | restoreFocus: false,
56 | trigger: button
57 | }) : button;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/menu/examples/default.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Icons, Menu, MenuDivider, MenuItem, Switch, Size } from '@/';
3 | import { Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/menu/examples/default.ts';
6 |
7 | export class MenuExample {
8 | private basic: boolean = false;
9 | private size: Size;
10 |
11 | public view() {
12 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
13 | m(Menu, { size: this.size, basic: this.basic }, [
14 | m(MenuItem, {
15 | iconLeft: Icons.COPY,
16 | label: 'Copy'
17 | }),
18 |
19 | m(MenuItem, {
20 | iconLeft: Icons.EDIT_2,
21 | label: 'Edit'
22 | }),
23 |
24 | m(MenuItem, {
25 | iconLeft: Icons.SETTINGS,
26 | label: 'Settings'
27 | }),
28 |
29 | m(MenuDivider),
30 |
31 | m(MenuItem, {
32 | iconLeft: Icons.TRASH_2,
33 | label: 'Delete',
34 | intent: 'negative'
35 | })
36 | ])
37 | ]);
38 | }
39 |
40 | private renderOptions() {
41 | return [
42 | m('h5', 'Size'),
43 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
44 | m(Switch, {
45 | checked: this.basic,
46 | label: 'Basic',
47 | onchange: () => this.basic = !this.basic
48 | })
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/menu/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default';
2 | export * from './submenu';
3 |
--------------------------------------------------------------------------------
/src/components/menu/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 | @import '../../_shared/mixins';
3 |
4 | $cui-menu-max-width-map: (
5 | xs: 140px,
6 | sm: 160px,
7 | lg: 200px,
8 | xl: 240px
9 | );
10 |
11 | @mixin menu-sizing($padding, $max-width) {
12 | $spacing: floor($padding * 0.5);
13 | max-width: $max-width;
14 | min-width: $max-width - 40;
15 | padding: $spacing 0;
16 |
17 | .cui-menu-divider {
18 | margin: $spacing 0;
19 | }
20 |
21 | .cui-menu-heading {
22 | padding: $spacing $padding;
23 | }
24 | }
25 |
26 | .cui-menu {
27 | @include menu-sizing($cui-base-padding, 180px);
28 | border-radius: $cui-border-radius;
29 | background:white;
30 | border: solid 1px $cui-base-border-color;
31 | display: inline-flex;
32 | vertical-align: middle;
33 | flex-direction: column;
34 |
35 | .cui-menu-item {
36 | border-radius: 0;
37 | }
38 |
39 | .cui-menu-divider {
40 | border-bottom: solid 1px $cui-base-border-color;
41 | }
42 |
43 | .cui-menu-heading {
44 | color: $blue-grey900;
45 | font-weight: bold;
46 | font-size: 12px;
47 | }
48 |
49 | &.cui-basic {
50 | border: none;
51 | }
52 |
53 | @each $size in $cui-sizes {
54 | &.cui-#{$size} {
55 | @include menu-sizing(
56 | map-get($cui-padding-map, $size),
57 | map-get($cui-menu-max-width-map, $size)
58 | )
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Menu';
2 | export * from './MenuDivider';
3 | export * from './MenuItem';
4 | export * from './MenuHeading';
5 |
--------------------------------------------------------------------------------
/src/components/menu/menu.md:
--------------------------------------------------------------------------------
1 | @# Menu
2 | A menu displays a list of menu items in vertical format.
3 | @example MenuExample
4 |
5 | @## Submenu
6 | A menu supports submenus by providing a `submenu` attribute on a menu item.
7 | @example MenuSubExample
8 |
9 | @## Menu Attrs
10 | @interface IMenuAttrs
11 |
12 | @## MenuItem Attrs
13 | @interface IMenuItemAttrs
14 |
--------------------------------------------------------------------------------
/src/components/overlay/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-overlay-backdrop {
4 | position: fixed;
5 | top: 0;
6 | right: 0;
7 | bottom: 0;
8 | left: 0;
9 | z-index: $cui-z-index-overlay;
10 | // TODO: switch to UI colors
11 | background-color: rgba(16, 22, 26, 0.7);
12 |
13 | .fade-enter & {
14 | opacity: 0.01;
15 | transition: opacity $cui-transition-duration $cui-transition-ease;
16 | }
17 |
18 | .fade-enter-active & {
19 | opacity: 1;
20 | }
21 |
22 | .fade-exit & {
23 | opacity: 0.01;
24 | transition: opacity $cui-transition-duration $cui-transition-ease;
25 | }
26 |
27 | .fade-exit-active & {
28 | opacity: 0;
29 | }
30 | }
31 |
32 | body.cui-overlay-open {
33 | overflow-y: hidden;
34 | }
35 |
36 | .cui-overlay-inline {
37 | .cui-overlay-backdrop {
38 | position:absolute;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/overlay/overlay.md:
--------------------------------------------------------------------------------
1 | @# Overlay
2 | An overlay is composed of a `Portal` component that allows for content to be rendered on top of it's siblings. By default, an `Overlay` will render it's content to an element appended to `document.body`. It is a controlled component and can only be toggled via the `isOpen` attribute.
3 |
4 | @example OverlayExample
5 |
6 | @## Overlay Attrs
7 | @interface IOverlayAttrs
8 |
--------------------------------------------------------------------------------
/src/components/popover-menu/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Icons, MenuDivider, MenuItem, Size, PopoverMenu, Button, Switch } from '@/';
3 | import { Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/popover-menu/examples/index.ts';
6 |
7 | export class PopoverMenuExample {
8 | private size: Size;
9 | private closeOnClick: boolean = true;
10 |
11 | public view() {
12 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
13 | m(PopoverMenu, {
14 | closeOnContentClick: this.closeOnClick,
15 | content: [
16 | m(MenuItem, {
17 | iconLeft: Icons.COPY,
18 | label: 'Copy'
19 | }),
20 |
21 | m(MenuItem, {
22 | iconLeft: Icons.EDIT_2,
23 | label: 'Edit'
24 | }),
25 |
26 | m(MenuItem, {
27 | iconLeft: Icons.SETTINGS,
28 | label: 'Settings'
29 | }),
30 |
31 | m(MenuDivider),
32 |
33 | m(MenuItem, {
34 | iconLeft: Icons.TRASH_2,
35 | label: 'Delete',
36 | intent: 'negative'
37 | })
38 | ],
39 | menuAttrs: { size: this.size },
40 | trigger: m(Button, { iconLeft: Icons.SETTINGS })
41 | })
42 | ]);
43 | }
44 |
45 | private renderOptions() {
46 | return [
47 | m('h5', 'Size'),
48 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
49 | m(Switch, {
50 | label: 'Close on select',
51 | onchange: () => this.closeOnClick = !this.closeOnClick,
52 | checked: this.closeOnClick
53 | })
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/popover-menu/index.scss:
--------------------------------------------------------------------------------
1 | .cui-popover-menu {
2 | .cui-popover-content {
3 | padding: 0;
4 | border: none;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/popover-menu/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, expect, it } from 'vitest';
3 | import { Classes, IPopoverMenuAttrs } from '@/';
4 | import { PopoverMenu } from '.';
5 | import { MenuItem } from '../menu';
6 | import { hasChildClass, hasClass } from '@shared/test/utils';
7 |
8 | describe('popover-menu', () => {
9 | const popover = () => document.body.querySelector(`.${Classes.POPOVER}`) as HTMLElement;
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | defaultIsOpen: true,
14 | content: [
15 | m(MenuItem, { label: 'Test' })
16 | ]
17 | });
18 |
19 | expect(hasClass(popover(), Classes.POPOVER_MENU)).toBeTruthy();
20 | expect(hasChildClass(popover(), Classes.MENU)).toBeTruthy();
21 | });
22 |
23 | function mount(attrs: Partial) {
24 | const component = {
25 | view: () => m(PopoverMenu, {
26 | transitionDuration: 0,
27 | trigger: m(''),
28 | content: '',
29 | ...attrs
30 | })
31 | };
32 |
33 | m.mount(document.body, component);
34 | return component;
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/popover-menu/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes } from '../../';
4 | import { IPopoverAttrs, Popover } from '../popover';
5 | import { IMenuAttrs, Menu } from '../menu';
6 |
7 | export interface IPopoverMenuAttrs extends IPopoverAttrs {
8 | /** Attrs passed through to Menu component */
9 | menuAttrs?: IMenuAttrs;
10 | }
11 |
12 | export class PopoverMenu implements m.Component {
13 | public view({ attrs }: m.Vnode) {
14 | const { class: className, menuAttrs, content, ...popoverAttrs } = attrs;
15 |
16 | return m(Popover, {
17 | ...popoverAttrs,
18 | class: classnames(Classes.POPOVER_MENU, className),
19 | content: m(Menu, { ...menuAttrs }, content)
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/popover-menu/popover-menu.md:
--------------------------------------------------------------------------------
1 | @# Popover Menu
2 | A popover menu provides a simple wrapper over a `Popover` and `Menu` component.
3 |
4 | @example PopoverMenuExample
5 |
6 | @## PopoverMenu Attrs
7 | @interface IPopoverMenuAttrs
8 |
--------------------------------------------------------------------------------
/src/components/popover/examples/controlled.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Switch, Popover, Button } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/popover/examples/controlled.ts';
6 |
7 | export class PopoverControlledExample {
8 | private isOpen = false;
9 |
10 | public view() {
11 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
12 | m(Popover, {
13 | closeOnContentClick: true,
14 | closeOnEscapeKey: true,
15 | content: m('', 'Popover content'),
16 | trigger: m(Button, {
17 | label: 'Popover Trigger',
18 | intent: 'primary'
19 | }),
20 | position: 'bottom',
21 | isOpen: this.isOpen,
22 | onInteraction: (nextOpenState: boolean) => this.isOpen = nextOpenState
23 | })
24 | ]);
25 | }
26 |
27 | private renderOptions() {
28 | return [
29 | m(Switch, {
30 | label: 'Is open',
31 | checked: this.isOpen,
32 | onchange: () => this.isOpen = !this.isOpen
33 | })
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/popover/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controlled';
2 | export * from './default';
3 | export * from './nested';
4 |
--------------------------------------------------------------------------------
/src/components/popover/examples/nested.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Popover, Switch, Button } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/popover/examples/nested.ts';
6 |
7 | export class PopoverNestedExample {
8 | private addToStack = true;
9 |
10 | public view() {
11 | const nestedPopover = m(Popover, {
12 | closeOnContentClick: true,
13 | content: 'Content',
14 | trigger: m(Button, { label: 'Nested trigger' }),
15 | position: 'left',
16 | inline: true,
17 | addToStack: this.addToStack
18 | });
19 |
20 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
21 | m(Popover, {
22 | content: nestedPopover,
23 | trigger: m(Button, {
24 | label: 'Popover trigger',
25 | intent: 'primary'
26 | })
27 | })
28 | ]);
29 | }
30 |
31 | private renderOptions() {
32 | return [
33 | m(Switch, {
34 | label: 'Add to stack',
35 | checked: this.addToStack,
36 | onchange: () => this.addToStack = !this.addToStack
37 | })
38 | ];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/popover/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Popover';
2 | export * from './popoverTypes';
3 |
--------------------------------------------------------------------------------
/src/components/popover/popover.md:
--------------------------------------------------------------------------------
1 | @# Popover
2 | A popover composes the `Overlay` component and allows for overlain content in relation to a `trigger` element. It leverages the positioning engine of Popper.js and allows content to "flip" sides based on its `position` and available space.
3 |
4 | @example PopoverExample
5 |
6 | @## Controlled State
7 | Setting the `isOpen` attribute will put the popover in controlled mode . Every event that modifies the open state will call the `onInteraction` callback with the `nextOpenState` and the calling `event` as it's parameters.
8 |
9 | @example PopoverControlledExample
10 |
11 | @## Nested Popovers
12 | By default, nested popovers are closed in sequence. Opening both parent and nested popover in the example below will require two clicks to dismiss them. To dismiss both at once, the nested popover must pass `inline: true` and `addToStack: false` so when its content is clicked, the parent popover doesn't close.
13 |
14 | @example PopoverNestedExample
15 |
16 | @## Popover Attrs
17 | @interface IPopoverAttrs
18 |
--------------------------------------------------------------------------------
/src/components/popover/popoverTypes.ts:
--------------------------------------------------------------------------------
1 | export const PopoverInteraction = {
2 | CLICK: 'click',
3 | CLICK_TRIGGER: 'click-trigger',
4 | HOVER: 'hover',
5 | HOVER_TRIGGER: 'hover-trigger'
6 | } as const;
7 |
8 | export type PopoverInteraction = typeof PopoverInteraction[keyof typeof PopoverInteraction];
9 |
10 | export const PopoverPosition = {
11 | AUTO: 'auto',
12 | AUTO_START: 'auto-start',
13 | AUTO_END: 'auto-end',
14 | TOP: 'top',
15 | TOP_START: 'top-start',
16 | TOP_END: 'top-end',
17 | RIGHT: 'right',
18 | RIGHT_START: 'right-start',
19 | RIGHT_END: 'right-end',
20 | BOTTOM: 'bottom',
21 | BOTTOM_START: 'bottom-start',
22 | BOTTOM_END: 'bottom-end',
23 | LEFT: 'left',
24 | LEFT_START: 'left-start',
25 | LEFT_END: 'left-end'
26 | } as const;
27 |
28 | export type PopoverPosition = typeof PopoverPosition[keyof typeof PopoverPosition];
29 |
--------------------------------------------------------------------------------
/src/components/portal/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Card, Portal, Button } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/portal/examples/index.ts';
6 |
7 | export class PortalExample {
8 | private isOpen = false;
9 |
10 | public view() {
11 | const card = m(Card, { elevation: 4 }, m('', 'This content is taken out of document flow.'));
12 |
13 | return m(Example, { src: EXAMPLE_SRC }, [
14 | m(Button, {
15 | intent: 'primary',
16 | label: this.isOpen ? 'Hide Portal' : 'Show Portal',
17 | onclick: () => this.isOpen = !this.isOpen
18 | }),
19 |
20 | this.isOpen && m(Portal, { style: 'position: absolute; top:10px; left: 10px' }, card)
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/portal/index.scss:
--------------------------------------------------------------------------------
1 | .cui-portal {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | left: 0;
6 | }
--------------------------------------------------------------------------------
/src/components/portal/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, safeCall, normalizeStyle } from '../../_shared';
4 |
5 | export interface IPortalAttrs extends IAttrs {
6 | /** Callback invoked when the component is mounted */
7 | onContentMount?: (rootElement: HTMLElement) => void;
8 |
9 | /** Optional HTML element to mount to */
10 | container?: HTMLElement;
11 | }
12 |
13 | export class Portal implements m.Component {
14 | private rootElement: HTMLElement;
15 | private content: m.Component;
16 |
17 | public oncreate({ attrs, children }: m.Vnode) {
18 | const rootElement = document.createElement('div');
19 | const container = attrs.container || document.body;
20 | container.appendChild(rootElement);
21 | this.rootElement = rootElement;
22 |
23 | this.setStyles(attrs);
24 |
25 | this.content = { view: () => children };
26 | m.mount(this.rootElement, this.content);
27 | safeCall(attrs.onContentMount, rootElement);
28 | }
29 |
30 | public onupdate({ attrs }: m.Vnode) {
31 | this.setStyles(attrs);
32 | }
33 |
34 | public onbeforeupdate({ children }: m.Vnode) {
35 | if (!this.content) return false;
36 | this.content.view = () => children;
37 | }
38 |
39 | public onremove({ attrs }: m.Vnode) {
40 | const container = attrs.container || document.body;
41 |
42 | if (container.contains(this.rootElement)) {
43 | m.mount(this.rootElement, null);
44 | container.removeChild(this.rootElement);
45 | }
46 | }
47 |
48 | public view() {
49 | return m.fragment({}, '');
50 | }
51 |
52 | private setStyles(attrs: IPortalAttrs) {
53 | this.rootElement.className = classnames(Classes.PORTAL, attrs.class);
54 | this.rootElement.style.cssText = '';
55 |
56 | if (attrs.style) {
57 | Object.assign(this.rootElement.style, normalizeStyle(attrs.style));
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/portal/portal.md:
--------------------------------------------------------------------------------
1 | @# Portal
2 |
3 | A portal mounts its children to a `div` element that is appended to `document.body` (or a container element if specified). This is useful for UI related components such as overlays, popovers, dropdowns, etc. where rendering inline would cause CSS overflow/z-index issues.
4 |
5 | @example PortalExample
6 |
7 | @# Portal Attrs
8 | @interface IPortalAttrs
9 |
--------------------------------------------------------------------------------
/src/components/query-list/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controlled';
2 | export * from './default';
3 |
--------------------------------------------------------------------------------
/src/components/query-list/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @import '../../_shared/variables';
4 |
5 | @mixin cui-query-list-sizing($padding) {
6 | .cui-list-item {
7 | padding-left: $padding * 3;
8 | }
9 |
10 | .cui-list-item-content-left {
11 | left: floor(math.div($padding, 1.2));
12 | }
13 | }
14 |
15 | .cui-query-list {
16 | outline: none;
17 |
18 | .cui-control-group {
19 | margin:0;
20 | margin-bottom: 15px;
21 |
22 | &.cui-fluid > * {
23 | flex: none;
24 | }
25 |
26 | .cui-input {
27 | flex-grow: 1;
28 | }
29 | }
30 |
31 | .cui-list-item {
32 | border-bottom: none;
33 | }
34 | }
35 |
36 | .cui-query-list-checkmark {
37 | @include cui-query-list-sizing($cui-base-padding);
38 |
39 | .cui-list-item-content-left {
40 | position: absolute;
41 | }
42 |
43 | .cui-selected {
44 | .cui-icon-check {
45 | color: $cui-primary-bg-color;
46 | }
47 | }
48 |
49 | @each $size in $cui-sizes {
50 | .cui-#{$size} {
51 | @include cui-query-list-sizing(map-get($cui-padding-map, $size),);
52 | }
53 | }
54 | }
55 |
56 | .cui-query-list-empty,
57 | .cui-query-list-initial {
58 | background: none;
59 |
60 | .cui-query-list-message {
61 | padding: 10px;
62 | color: $blue-grey500;
63 | font-size: $cui-font-size;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/query-list/query-list.md:
--------------------------------------------------------------------------------
1 | @# Query List
2 | A query list allows an array of items to be filtered by a query string. It is composed of a `ControlGroup`, `Input` and `List` component and allows for a number of keyboard interactions:
3 | + The `UP` and `DOWN` arrow keys navigate through the list.
4 | + The `ENTER` key selects an item.
5 | + The `ESCAPE` key clears the query value in the input field.
6 |
7 | @example QueryListExample
8 |
9 | @## Controlled mode
10 | The QueryList can be used in a controlled mode by passing a `query` and/or `activeIndex` attribute. When the `query` attribute is defined, the `onQueryChange` callback is invoked to change the input search value. Similarly, if `activeIndex` is defined, the `onActiveItemChange` is called whenever the active index is to be updated.
11 | @example QueryListControlledExample
12 |
13 | @## QueryList Attrs
14 | @interface IQueryListAttrs
15 |
--------------------------------------------------------------------------------
/src/components/radio/Radio.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Classes } from '../../_shared';
3 | import { BaseControl, IControlAttrs } from '../base-control';
4 |
5 | export class Radio implements m.Component {
6 | public view({ attrs }: m.Vnode) {
7 | return m(BaseControl, {
8 | ...attrs,
9 | type: 'radio',
10 | typeClass: Classes.RADIO
11 | });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/radio/examples/default.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Radio, Switch, Intent, Size } from '@/';
3 | import { IntentSelect, Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/radio/examples/default.ts';
6 |
7 | export class RadioExample {
8 | private disabled: boolean;
9 | private intent: Intent;
10 | private label: boolean = true;
11 | private readonly: boolean;
12 | private size: Size;
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
16 | m(Radio, {
17 | disabled: this.disabled,
18 | label: this.label && 'Radio label',
19 | intent: this.intent,
20 | readonly: this.readonly,
21 | size: this.size
22 | })
23 | ]);
24 | }
25 |
26 | private renderOptions() {
27 | return [
28 | m('h5', 'Sizes'),
29 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
30 | m('h5', 'Intent'),
31 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
32 | m(Switch, {
33 | checked: this.disabled,
34 | label: 'Disabled',
35 | onchange: () => this.disabled = !this.disabled
36 | }),
37 |
38 | m(Switch, {
39 | checked: this.label,
40 | label: 'Label',
41 | onchange: () => this.label = !this.label
42 | }),
43 |
44 | m(Switch, {
45 | checked: this.readonly,
46 | label: 'Readonly',
47 | onchange: () => this.readonly = !this.readonly
48 | })
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/radio/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default';
2 | export * from './radio-group';
3 |
--------------------------------------------------------------------------------
/src/components/radio/examples/radio-group.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { RadioGroup } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/radio/examples/radio-group.ts';
6 |
7 | export class RadioGroupExample {
8 | private options = ['First', 'Second', 'Third'];
9 | private selected = 'Second';
10 |
11 | public view() {
12 | return m(Example, { src: EXAMPLE_SRC }, [
13 | m(RadioGroup, {
14 | options: this.options,
15 | value: this.selected,
16 | onchange: (e: Event) => this.selected = (e.currentTarget as HTMLInputElement).value
17 | })
18 | ]);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/radio/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Radio';
2 | export * from './RadioGroup';
3 |
--------------------------------------------------------------------------------
/src/components/radio/radio.md:
--------------------------------------------------------------------------------
1 | @# Radio
2 | A radio is a form control that allows for single select options.
3 |
4 | @example RadioExample
5 |
6 | @## RadioGroup
7 | A radio group arranges a list of radio controls.
8 | @example RadioGroupExample
9 |
10 | @## Radio Attrs
11 | @interface IControlAttrs
12 |
13 | @## RadioGroup Attrs
14 | @interface IRadioGroupAttrs
15 |
--------------------------------------------------------------------------------
/src/components/select-list/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './default';
2 | export * from './multiple';
3 |
--------------------------------------------------------------------------------
/src/components/select-list/examples/multiple.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { ListItem, SelectList, Button, Icons } from '@/';
3 | import { Example, countries, ICountryModel } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/select-list/examples/multiple.ts';
6 | const CountryList = SelectList.ofType();
7 |
8 | export class SelectListMultipleExample {
9 | private selectedItems: Map = new Map();
10 |
11 | public view() {
12 | return m(Example, { src: EXAMPLE_SRC }, [
13 | m(CountryList, {
14 | closeOnSelect: false,
15 | items: countries,
16 | itemRender: this.renderItem,
17 | itemPredicate: this.itemPredicate,
18 | onSelect: this.handleSelect,
19 | popoverAttrs: { hasArrow: false },
20 | trigger: m(Button, {
21 | align: 'left',
22 | compact: true,
23 | iconRight: Icons.CHEVRON_DOWN,
24 | sublabel: 'Country:',
25 | label: `${this.selectedItems.size} selected`,
26 | style: 'min-width: 300px'
27 | })
28 | })
29 | ]);
30 | }
31 |
32 | private renderItem = (item: ICountryModel) => m(ListItem, {
33 | label: item.name,
34 | selected: this.selectedItems.has(item.name)
35 | });
36 |
37 | private itemPredicate(query: string, item: ICountryModel) {
38 | return item.name.toLowerCase().includes(query.toLowerCase());
39 | }
40 |
41 | private handleSelect = (item: ICountryModel) => {
42 | if (this.selectedItems.has(item.name)) {
43 | this.selectedItems.delete(item.name);
44 | } else this.selectedItems.set(item.name, item);
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/select-list/select-list.md:
--------------------------------------------------------------------------------
1 | @# Select List
2 | A select list is composed of a `Popover` and `QueryList` component and can be used as a replacement for a native select.
3 |
4 | @example SelectListExample
5 |
6 |
7 | @## Multiple select
8 | @example SelectListMultipleExample
9 |
10 | @## SelectList Attrs
11 | @interface ISelectListAttrs
12 |
--------------------------------------------------------------------------------
/src/components/select/examples/controlled.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Select, Button } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/select/examples/controlled.ts';
6 |
7 | export class SelectControlledExample {
8 | private options: string[] = ['First', 'Second', 'Third', 'Fourth'];
9 | private value: string = 'Third';
10 |
11 | public view() {
12 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
13 | m(Select, {
14 | options: this.options,
15 | onchange: () => null,
16 | value: this.value
17 | })
18 | ]);
19 | }
20 |
21 | private renderOptions() {
22 | return [
23 | m(Button, {
24 | label: 'Set to "Second"',
25 | size: 'xs',
26 | onclick: () => this.value = 'Second',
27 | style: 'margin-bottom: 10px'
28 | }),
29 |
30 | m(Button, {
31 | label: 'Set to "Fourth"',
32 | size: 'xs',
33 | onclick: () => this.value = 'Fourth'
34 | })
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/select/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controlled';
2 | export * from './default';
3 |
--------------------------------------------------------------------------------
/src/components/select/select.md:
--------------------------------------------------------------------------------
1 | @# Select
2 | A select displays a list of selectable options.
3 |
4 | @example SelectDefaultExample
5 |
6 | @## Controlled select
7 | A "controlled" select can only be toggled through external state.
8 | @example SelectControlledExample
9 |
10 | @## Select Attrs
11 | @interface ISelectAttrs
12 |
--------------------------------------------------------------------------------
/src/components/spinner/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Intent, Size, Spinner, Switch } from '@/';
3 | import { IntentSelect, SizeSelect, Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/spinner/examples/index.ts';
6 |
7 | export class SpinnerExample {
8 | private size: Size;
9 | private intent: Intent;
10 | private active: boolean = true;
11 | private fill: boolean;
12 | private background: boolean;
13 | private hasMessage: boolean = false;
14 |
15 | public view() {
16 | return m(Example, { options: this.renderOptions(), center: false, src: EXAMPLE_SRC }, [
17 | m(Spinner, {
18 | active: this.active,
19 | background: this.background,
20 | fill: this.fill,
21 | intent: this.intent,
22 | size: this.size,
23 | message: this.hasMessage ? 'Uploading files...' : undefined
24 | })
25 | ]);
26 | }
27 |
28 | public renderOptions() {
29 | return [
30 | m('h5', 'Size'),
31 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
32 | m('h5', 'Intent'),
33 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
34 | m(Switch, {
35 | checked: this.active,
36 | label: 'Active',
37 | onchange: () => this.active = !this.active
38 | }),
39 |
40 | m(Switch, {
41 | checked: this.fill,
42 | label: 'Fill container',
43 | onchange: () => this.fill = !this.fill
44 | }),
45 |
46 | m(Switch, {
47 | checked: this.background,
48 | label: 'Has background',
49 | onchange: () => this.background = !this.background
50 | }),
51 |
52 | m(Switch, {
53 | checked: this.hasMessage,
54 | label: 'Message',
55 | onchange: () => this.hasMessage = !this.hasMessage
56 | })
57 | ];
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/spinner/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Spinner, ISpinnerAttrs, Classes } from '@/';
4 | import { hasClass, hasChildClass } from '@test-utils';
5 |
6 | describe('spinner', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | active: true,
14 | background: true,
15 | fill: true,
16 | class: Classes.POSITIVE,
17 | intent: 'primary',
18 | size: 'xs',
19 | style: 'color: red'
20 | });
21 |
22 | expect(hasClass(el(), Classes.SPINNER_ACTIVE)).toBeTruthy();
23 | expect(hasClass(el(), Classes.SPINNER_BG)).toBeTruthy();
24 | expect(hasClass(el(), Classes.SPINNER_FILL)).toBeTruthy();
25 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
26 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
27 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
28 | expect(el().hasAttribute('style')).toBeTruthy();
29 |
30 | expect(hasChildClass(el(), Classes.SPINNER_CONTENT)).toBeTruthy();
31 | expect(hasChildClass(el(), Classes.SPINNER_ICON)).toBeTruthy();
32 | });
33 |
34 | it('Renders message', () => {
35 | const message = 'Uploading files';
36 | mount({ message });
37 |
38 | expect(hasChildClass(el(), Classes.SPINNER_MESSAGE)).toBeTruthy();
39 |
40 | const messageEl = el().querySelector(`.${Classes.SPINNER_MESSAGE}`) as HTMLElement;
41 | expect(messageEl.innerHTML).toBe(message);
42 | });
43 |
44 | it('Passes through html attrs', () => {
45 | mount({
46 | id: 1,
47 | name: 'name'
48 | });
49 |
50 | expect(el().hasAttribute('id')).toBeTruthy();
51 | expect(el().hasAttribute('name')).toBeTruthy();
52 | });
53 |
54 | function mount(attrs?: ISpinnerAttrs) {
55 | const component = {
56 | view: () => m(Spinner, { ...attrs })
57 | };
58 | m.mount(document.body, component);
59 | }
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/spinner/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 |
5 | export interface ISpinnerAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
6 | /** Toggles visibility of spinner */
7 | active?: boolean;
8 |
9 | /** Fills the height/width of parent container */
10 | fill?: boolean;
11 |
12 | /** Shows background when fill=true */
13 | background?: boolean;
14 |
15 | /** Optional message to show under icon */
16 | message?: string;
17 |
18 | [htmlAttrs: string]: any;
19 | }
20 |
21 | export class Spinner implements m.Component {
22 | public view({ attrs }: m.Vnode) {
23 | const {
24 | active,
25 | background,
26 | class: className,
27 | fill,
28 | intent,
29 | message,
30 | size,
31 | ...otherAttrs
32 | } = attrs;
33 |
34 | const content = [
35 | m(`.${Classes.SPINNER_CONTENT}`, [
36 | m(`.${Classes.SPINNER_ICON}`),
37 | message && m(`.${Classes.SPINNER_MESSAGE}`, message)
38 | ])
39 | ];
40 |
41 | return m('', {
42 | ...otherAttrs,
43 | class: classnames(
44 | Classes.SPINNER,
45 | active && Classes.SPINNER_ACTIVE,
46 | background && Classes.SPINNER_BG,
47 | fill && Classes.SPINNER_FILL,
48 | intent && `cui-${attrs.intent}`,
49 | size && `cui-${attrs.size}`,
50 | className
51 | )
52 | }, content);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/spinner/spinner.md:
--------------------------------------------------------------------------------
1 | @# Spinner
2 | A spinner shows a loading indicator.
3 |
4 | @example SpinnerExample
5 |
6 | @## Attrs
7 | @interface ISpinnerAttrs
8 |
--------------------------------------------------------------------------------
/src/components/switch/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Switch, Size, Intent } from '@/';
3 | import { IntentSelect, Example, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/switch/examples/index.ts';
6 |
7 | export class SwitchExample {
8 | private disabled: boolean = false;
9 | private intent: Intent;
10 | private label: boolean = true;
11 | private readonly: boolean = false;
12 | private size: Size;
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
16 | m(Switch, {
17 | disabled: this.disabled,
18 | label: this.label && 'Switch label',
19 | intent: this.intent,
20 | readonly: this.readonly,
21 | size: this.size
22 | })
23 | ]);
24 | }
25 |
26 | private renderOptions() {
27 | return [
28 | m('h5', 'Sizes'),
29 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
30 | m('h5', 'Intent'),
31 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
32 | m(Switch, {
33 | checked: this.disabled,
34 | label: 'Disabled',
35 | onchange: () => this.disabled = !this.disabled
36 | }),
37 |
38 | m(Switch, {
39 | checked: this.label,
40 | label: 'Label',
41 | onchange: () => this.label = !this.label
42 | }),
43 |
44 | m(Switch, {
45 | checked: this.readonly,
46 | label: 'Readonly',
47 | onchange: () => this.readonly = !this.readonly
48 | })
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/switch/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | @mixin cui-switch-sizing($size) {
4 | padding-left: floor($size * 2.5);
5 |
6 | .cui-control-indicator {
7 | height: $size;
8 | width: floor($size * 2);
9 |
10 | &:after {
11 | height: $size;
12 | width: $size;
13 | }
14 | }
15 |
16 | input:checked ~ .cui-control-indicator:after {
17 | left: $size;
18 | }
19 | }
20 |
21 | @mixin cui-switch-style($bg-color, $bg-color-hover, $bg-color-active) {
22 | .cui-control-indicator {
23 | background: $bg-color;
24 | }
25 |
26 | &:hover .cui-control-indicator {
27 | border:none;
28 | background: $bg-color-hover;
29 | }
30 |
31 | input:checked ~ .cui-control-indicator {
32 | background: $bg-color-active
33 | }
34 | }
35 |
36 | .cui-switch {
37 | @include cui-switch-sizing($cui-control-base);
38 | @include cui-switch-style(
39 | $cui-base-bg-color-active,
40 | shade($cui-base-bg-color-active, 10%),
41 | $cui-primary-bg-color
42 | );
43 |
44 | .cui-control-indicator {
45 | border: none;
46 | border-radius: 40px;
47 |
48 | &:after {
49 | position:relative;
50 | display:block;
51 | top:0;
52 | left:0;
53 | content: '';
54 | border-radius: 40px;
55 | background:white;
56 | border: solid 1px $cui-base-border-color;
57 | transition:left $cui-transition-duration $cui-transition-ease;
58 | }
59 | }
60 |
61 | &:hover .cui-control-indicator {
62 | border: none;
63 | }
64 |
65 | @each $size in $cui-sizes {
66 | &.cui-#{$size} {
67 | @include cui-switch-sizing(map-get($cui-control-map, $size))
68 | }
69 | }
70 |
71 | @each $intent in $cui-intents {
72 | &.cui-#{$intent} {
73 | @include cui-switch-style(
74 | tint(map-get($cui-bg-color-map, $intent), 50%),
75 | tint(map-get($cui-bg-color-map, $intent), 30%),
76 | map-get($cui-bg-color-map, $intent)
77 | )
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/switch/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Classes } from '../../_shared';
3 | import { BaseControl, IControlAttrs } from '../base-control';
4 |
5 | export class Switch implements m.Component {
6 | public view({ attrs }: m.Vnode) {
7 | return m(BaseControl, {
8 | ...attrs,
9 | type: 'checkbox',
10 | typeClass: Classes.SWITCH
11 | });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/switch/switch.md:
--------------------------------------------------------------------------------
1 | @# Switch
2 | A switch is a form control that can be in either an on or off state.
3 |
4 | @example SwitchExample
5 |
6 | @## Switch Attrs
7 | @interface IControlAttrs
8 |
--------------------------------------------------------------------------------
/src/components/table/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Table, Switch } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/table/examples/index.ts';
6 |
7 | export class TableExample {
8 | private bordered = true;
9 | private interactive = true;
10 | private striped = false;
11 |
12 | public view() {
13 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
14 | m(Table, {
15 | bordered: this.bordered,
16 | interactive: this.interactive,
17 | striped: this.striped
18 | }, [
19 | m('tr', [
20 | m('th', 'Heading 1'),
21 | m('th', 'Heading 2')
22 | ]),
23 | m('tr', [
24 | m('td', 'Cell 1'),
25 | m('td', 'Cell 2')
26 | ]),
27 | m('tr', [
28 | m('td', 'Cell 1'),
29 | m('td', 'Cell 2')
30 | ]),
31 | m('tr', [
32 | m('td', 'Cell 1'),
33 | m('td', 'Cell 2')
34 | ])
35 | ])
36 | ]);
37 | }
38 |
39 | private renderOptions() {
40 | return [
41 | m(Switch, {
42 | checked: this.bordered,
43 | label: 'Bordered',
44 | onchange: () => this.bordered = !this.bordered
45 | }),
46 | m(Switch, {
47 | checked: this.striped,
48 | label: 'Striped',
49 | onchange: () => this.striped = !this.striped
50 | }),
51 | m(Switch, {
52 | checked: this.interactive,
53 | label: 'Interactive',
54 | onchange: () => this.interactive = !this.interactive
55 | })
56 | ];
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/table/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 |
3 | .cui-table {
4 | border-collapse: collapse;
5 | border-spacing: 0;
6 | width: 100%;
7 | max-width: 100%;
8 | background-color: $white;
9 |
10 | tbody {
11 | width: 100%;
12 | }
13 |
14 | th, td {
15 | border-bottom: solid 1px $cui-base-border-color;
16 | padding: $cui-base-size;
17 | font-size: $cui-font-size;
18 | text-align:left;
19 | }
20 |
21 | &:not(.cui-table-bordered) tr:last-child {
22 | td, th { border-bottom: none}
23 | }
24 |
25 | &.cui-table-bordered {
26 | td, th {
27 | border: solid 1px $cui-base-border-color;
28 | }
29 | }
30 |
31 | &.cui-table-striped {
32 | tr:nth-of-type(odd) {
33 | background: $cui-hover-color;
34 | }
35 | }
36 |
37 | &.cui-table-interactive {
38 | tr {
39 | cursor: pointer;
40 | }
41 |
42 | tr:hover {
43 | background: $cui-hover-color;
44 | }
45 |
46 | tr:active {
47 | background: $blue-grey50;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/table/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Table, ITableAttrs, Classes } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('table', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | bordered: true,
15 | interactive: true,
16 | striped: true,
17 | style: 'color: red'
18 | });
19 |
20 | expect(hasClass(el(), Classes.TABLE)).toBeTruthy();
21 | expect(hasClass(el(), Classes.TABLE_BORDERED)).toBeTruthy();
22 | expect(hasClass(el(), Classes.TABLE_INTERACTIVE)).toBeTruthy();
23 | expect(hasClass(el(), Classes.TABLE_STRIPED)).toBeTruthy();
24 |
25 | expect(el().hasAttribute('style')).toBeTruthy();
26 | });
27 |
28 | it('Passes through html attrs', () => {
29 | mount({
30 | id: 1,
31 | name: 'name'
32 | });
33 |
34 | expect(el().hasAttribute('id')).toBeTruthy();
35 | expect(el().hasAttribute('name')).toBeTruthy();
36 | });
37 |
38 | function mount(attrs: ITableAttrs, children?: m.Children) {
39 | const component = {
40 | view: () => m(Table, { ...attrs }, children)
41 | };
42 | m.mount(document.body, component);
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/src/components/table/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs } from '../../_shared';
4 |
5 | export interface ITableAttrs extends IAttrs {
6 | /** Toggles bordered styling */
7 | bordered?: boolean;
8 |
9 | /** Adds interactive hover/active styling for each row */
10 | interactive?: boolean;
11 |
12 | /** Toggles striped styling */
13 | striped?: boolean;
14 |
15 | [htmlAttrs: string]: any;
16 | }
17 |
18 | export class Table implements m.Component {
19 | public view({ attrs, children }: m.Vnode) {
20 | const { class: className, bordered, interactive, striped, ...htmlAttrs } = attrs;
21 |
22 | return m('table', {
23 | ...htmlAttrs,
24 | class: classnames(
25 | Classes.TABLE,
26 | bordered && Classes.TABLE_BORDERED,
27 | striped && Classes.TABLE_STRIPED,
28 | interactive && Classes.TABLE_INTERACTIVE,
29 | className
30 | )
31 | }, children);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/table/table.md:
--------------------------------------------------------------------------------
1 | @# Table
2 | The table component provides a simple wrapper over the HTML `table` element.
3 |
4 | @example TableExample
5 |
6 | @# Table Attrs
7 | @interface ITableAttrs
8 |
--------------------------------------------------------------------------------
/src/components/tabs/Tabs.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { ISizeAttrs, IAttrs, Classes, Align } from '../../_shared';
4 |
5 | export interface ITabsAttrs extends IAttrs, ISizeAttrs {
6 | /**
7 | * Content alignment; Used to align tabs when fluid=true
8 | * @default 'center'
9 | */
10 | align?: Align;
11 |
12 | /** Toggles bottom border */
13 | bordered?: boolean;
14 |
15 | /** Fills width of parent container */
16 | fluid?: boolean;
17 |
18 | [htmlAttrs: string]: any;
19 | }
20 |
21 | export class Tabs implements m.ClassComponent {
22 | public view({ attrs, children }: m.Vnode) {
23 | const { align, bordered, fluid, size, class: classname, ...htmlAttrs } = attrs;
24 |
25 | return m('', {
26 | ...htmlAttrs,
27 | class: classnames(
28 | Classes.TABS,
29 | align && `cui-align-${align}`,
30 | bordered && Classes.TABS_BORDERED,
31 | fluid && Classes.FLUID,
32 | size && `cui-${size}`,
33 | classname
34 | )
35 | }, children);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsItem.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { IAttrs, Classes } from '../../_shared';
4 | import { Spinner } from '../spinner';
5 |
6 | export interface ITabItemAttrs extends IAttrs {
7 | /** Toggles active state */
8 | active?: boolean;
9 |
10 | /** Inner text or children */
11 | label: m.Children;
12 |
13 | /** Toggles loading animation */
14 | loading?: boolean;
15 |
16 | [htmlAttrs: string]: any;
17 | }
18 |
19 | export class TabItem implements m.ClassComponent {
20 | public view({ attrs }: m.Vnode) {
21 | const {
22 | active,
23 | label,
24 | loading,
25 | size,
26 | class: className,
27 | ...htmlAttrs
28 | } = attrs;
29 |
30 | const classes = classnames(
31 | Classes.TABS_ITEM,
32 | active && Classes.ACTIVE,
33 | loading && Classes.LOADING,
34 | size && `cui-${size}`,
35 | className
36 | );
37 |
38 | const content = [
39 | loading && m(Spinner, { active: true, fill: true }),
40 | label
41 | ];
42 |
43 | return m('', {
44 | class: classes,
45 | ...htmlAttrs
46 | }, content);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/tabs/_index.scss:
--------------------------------------------------------------------------------
1 | @mixin cui-tabs-size($padding, $font-size) {
2 | .cui-tabs-item {
3 | font-size: $font-size;
4 | padding: $padding;
5 | }
6 | }
7 |
8 | .cui-tabs {
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 |
13 | @include cui-tabs-size($cui-base-padding, $cui-font-size);
14 |
15 | &.cui-tabs-bordered {
16 | border-bottom: solid 1px $cui-base-border-color;
17 | }
18 |
19 | &.cui-align-left {
20 | justify-content: flex-start;
21 | }
22 |
23 | &.cui-align-right {
24 | justify-content: flex-end;
25 | }
26 |
27 | @each $size in $cui-sizes {
28 | &.cui-#{$size} {
29 | @include cui-tabs-size(
30 | map-get($cui-padding-map, $size),
31 | map-get($cui-font-size-map, $size)
32 | )
33 | }
34 | }
35 | }
36 |
37 | .cui-tabs-item {
38 | position: relative;
39 | display: flex;
40 | align-items: center;
41 | color: $blue-grey300;
42 | font-weight: bold;
43 | cursor: pointer;
44 | border-bottom: solid 2px transparent;
45 | transition:
46 | color $cui-transition-duration $cui-transition-ease,
47 | border $cui-transition-duration $cui-transition-ease;
48 |
49 | .cui-icon {
50 | color: $blue-grey300;
51 | }
52 |
53 | &:hover, &:hover .cui-icon {
54 | color: $blue-grey500;
55 | }
56 |
57 | &.cui-active {
58 | color: $cui-primary-bg-color;
59 | border-bottom: solid 2px $cui-primary-bg-color;
60 | }
61 |
62 | &.cui-active .cui-icon {
63 | color: $cui-primary-bg-color;
64 | }
65 |
66 | &.cui-loading {
67 | pointer-events: none;
68 | visibility: hidden;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/tabs/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Tabs';
2 | export * from './TabsItem';
3 |
--------------------------------------------------------------------------------
/src/components/tabs/tabs.md:
--------------------------------------------------------------------------------
1 | @# Tabs
2 | @example TabsExample
3 |
4 | @## Tabs Attrs
5 | @interface ITabsAttrs
6 |
7 | @## TabItem Attrs
8 | @interface ITabItemAttrs
9 |
--------------------------------------------------------------------------------
/src/components/tag-input/tag-input.md:
--------------------------------------------------------------------------------
1 | @# Tag Input
2 | A tag input renders a list of `Tags` and an input field for adding new entries.
3 |
4 | @example TagInputExample
5 |
6 | @## TagInput Attrs
7 | @interface ITagInputAttrs
8 |
--------------------------------------------------------------------------------
/src/components/tag/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Switch, Tag, Size, Intent } from '@/';
3 | import { IntentSelect, SizeSelect, Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/tag/examples/index.ts';
6 |
7 | export class TagExample {
8 | private size: Size;
9 | private intent: Intent;
10 | private removable = false;
11 | private rounded = false;
12 |
13 | public view() {
14 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
15 | m(Tag, {
16 | label: 'Tag Label',
17 | intent: this.intent,
18 | rounded: this.rounded,
19 | size: this.size,
20 | onRemove: this.removable ? () => null : undefined
21 | })
22 | ]);
23 | }
24 |
25 | public renderOptions() {
26 | return [
27 | m('h5', 'Sizes'),
28 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
29 | m('h5', 'Intent'),
30 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
31 | m(Switch, {
32 | checked: this.removable,
33 | label: 'Removable',
34 | onchange: () => this.removable = !this.removable
35 | }),
36 | m(Switch, {
37 | checked: this.rounded,
38 | label: 'Rounded',
39 | onchange: () => this.rounded = !this.rounded
40 | })
41 | ];
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/tag/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { Tag, ITagAttrs, Icons, Classes } from '@/';
4 | import { hasClass, hasChildClass } from '@test-utils';
5 |
6 | describe('tag', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 |
9 | afterEach(() => m.mount(document.body, null));
10 |
11 | it('Renders correctly', () => {
12 | mount({
13 | class: Classes.POSITIVE,
14 | intent: 'primary',
15 | rounded: true,
16 | size: 'xs',
17 | style: 'color: red'
18 | });
19 |
20 | expect(hasClass(el(), Classes.TAG)).toBeTruthy();
21 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
22 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
23 | expect(hasClass(el(), Classes.ROUNDED)).toBeTruthy();
24 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
25 |
26 | expect(el().hasAttribute('style')).toBeTruthy();
27 | });
28 |
29 | it('Renders label', () => {
30 | mount({
31 | label: 'label'
32 | });
33 |
34 | expect(el().textContent?.includes('label')).toBeTruthy();
35 | });
36 |
37 | it('Passes through html attrs', () => {
38 | mount({
39 | id: 1,
40 | name: 'name'
41 | });
42 |
43 | expect(el().hasAttribute('id')).toBeTruthy();
44 | expect(el().hasAttribute('name')).toBeTruthy();
45 | });
46 |
47 | it('Remove icon visible when onRemove specified', () => {
48 | mount({ onRemove: () => null });
49 | expect(hasChildClass(el(), `${Classes.ICON}-${Icons.X}`)).toBeTruthy();
50 | });
51 |
52 | it('onRemove called when remove icon clicked', () => {
53 | let count = 0;
54 | mount({ onRemove: () => count++ });
55 |
56 | const icon = el().querySelector(`.${Classes.ICON}`);
57 | icon!.dispatchEvent(new Event('click'));
58 |
59 | expect(count).toBe(1);
60 | });
61 |
62 | function mount(attrs: ITagAttrs) {
63 | const component = {
64 | view: () => m(Tag, { ...attrs })
65 | };
66 | m.mount(document.body, component);
67 | }
68 | });
69 |
--------------------------------------------------------------------------------
/src/components/tag/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 | import { Icon, Icons } from '../icon';
5 |
6 | export interface ITagAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
7 | /** Text label */
8 | label?: m.Children;
9 |
10 | /**
11 | * Callback invoked when "remove" icon is clicked;
12 | * Omitting this property will hide the remove icon.
13 | */
14 | onRemove?: (e: Event) => void;
15 |
16 | /** Toggles rounded styling */
17 | rounded?: boolean;
18 |
19 | [htmlAttrs: string]: any;
20 | }
21 |
22 | export class Tag implements m.Component {
23 | public view({ attrs }: m.Vnode) {
24 | const {
25 | class: className,
26 | label,
27 | intent,
28 | size,
29 | rounded,
30 | onRemove,
31 | ...htmlAttrs
32 | } = attrs;
33 |
34 | const classes = classnames(
35 | Classes.TAG,
36 | intent && `cui-${intent}`,
37 | rounded && Classes.ROUNDED,
38 | onRemove && Classes.TAG_REMOVABLE,
39 | size && `cui-${size}`,
40 | className
41 | );
42 |
43 | const content = [
44 | label,
45 | onRemove && m(Icon, {
46 | name: Icons.X,
47 | onclick: onRemove
48 | })
49 | ];
50 |
51 | return m('span', { ...htmlAttrs, class: classes }, content);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/tag/tag.md:
--------------------------------------------------------------------------------
1 | @# Tag
2 | A tag component.
3 |
4 | @example TagExample
5 |
6 | @## Tag Attrs
7 | @interface ITagAttrs
8 |
--------------------------------------------------------------------------------
/src/components/text-area/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { TextArea, Switch, Size, Intent } from '@/';
3 | import { IntentSelect, SizeSelect, Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/text-area/examples/index.ts';
6 |
7 | export class TextAreaExample {
8 | private basic: boolean = false;
9 | private disabled: boolean = false;
10 | private fluid: boolean = false;
11 | private intent: Intent;
12 | private readonly: boolean = false;
13 | private size: Size;
14 |
15 | public view() {
16 | return m(Example, { options: this.renderOptions(), center: false, src: EXAMPLE_SRC }, [
17 | m(TextArea, {
18 | basic: this.basic,
19 | disabled: this.disabled,
20 | fluid: this.fluid,
21 | intent: this.intent,
22 | placeholder: 'Placeholder...',
23 | readonly: this.readonly,
24 | size: this.size
25 | })
26 | ]);
27 | }
28 |
29 | private renderOptions() {
30 | return [
31 | m('h5', 'Sizes'),
32 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
33 | m('h5', 'Intent'),
34 | m(IntentSelect, { onSelect: (intent: Intent) => this.intent = intent }),
35 | m(Switch, {
36 | checked: this.disabled,
37 | label: 'Disabled',
38 | onchange: () => this.disabled = !this.disabled
39 | }),
40 |
41 | m(Switch, {
42 | checked: this.readonly,
43 | label: 'Readonly',
44 | onchange: () => this.readonly = !this.readonly
45 | }),
46 |
47 | m(Switch, {
48 | checked: this.basic,
49 | label: 'Basic',
50 | onchange: () => this.basic = !this.basic
51 | }),
52 |
53 | m(Switch, {
54 | checked: this.fluid,
55 | label: 'Fluid',
56 | onchange: () => this.fluid = !this.fluid
57 | })
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/text-area/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/_mixins';
2 | @import '../../_shared/_variables';
3 |
4 | @mixin cui-textarea-sizing($padding) {
5 | textarea {
6 | padding-top: $padding;
7 | padding-bottom: $padding;
8 | }
9 | }
10 |
11 | .cui-text-area {
12 | position: relative;
13 | @include cui-textarea-sizing($cui-base-padding);
14 |
15 | textarea {
16 | resize: both;
17 | height: initial !important;
18 | line-height: initial !important;
19 | }
20 |
21 | @each $size in $cui-sizes {
22 | &.cui-#{$size} {
23 | @include cui-textarea-sizing(map-get($cui-padding-map, $size))
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/text-area/index.spec.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { describe, afterEach, expect, it } from 'vitest';
3 | import { TextArea, ITextAreaAttrs, Classes } from '@/';
4 | import { hasClass } from '@test-utils';
5 |
6 | describe('textarea', () => {
7 | const el = () => document.body.firstChild as HTMLElement;
8 | const textarea = () => el().querySelector('textarea') as HTMLTextAreaElement;
9 |
10 | afterEach(() => m.mount(document.body, null));
11 |
12 | it('Renders correctly', () => {
13 | mount({
14 | basic: true,
15 | class: Classes.POSITIVE,
16 | style: 'color:red',
17 | intent: 'primary',
18 | size: 'xs',
19 | fluid: true
20 | });
21 |
22 | expect(hasClass(el(), Classes.INPUT)).toBeTruthy();
23 | expect(hasClass(el(), Classes.BASIC)).toBeTruthy();
24 | expect(hasClass(el(), Classes.POSITIVE)).toBeTruthy();
25 | expect(hasClass(el(), Classes.PRIMARY)).toBeTruthy();
26 | expect(hasClass(el(), Classes.XS)).toBeTruthy();
27 | expect(hasClass(el(), Classes.FLUID)).toBeTruthy();
28 | expect(el().hasAttribute('style')).toBeTruthy();
29 | });
30 |
31 | it('Passes through html attrs', () => {
32 | mount({
33 | id: 1,
34 | name: 'name',
35 | defaultValue: 'defaultValue'
36 | });
37 |
38 | expect(textarea().hasAttribute('id')).toBeTruthy();
39 | expect(textarea().hasAttribute('name')).toBeTruthy();
40 | expect(textarea().value).toBe('defaultValue');
41 | });
42 |
43 | function mount(attrs: ITextAreaAttrs) {
44 | const component = {
45 | view: () => m(TextArea, { ...attrs })
46 | };
47 | m.mount(document.body, component);
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/src/components/text-area/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs, ISizeAttrs, IIntentAttrs } from '../../_shared';
4 |
5 | export interface ITextAreaAttrs extends IAttrs, ISizeAttrs, IIntentAttrs {
6 | /** Toggles basic styling (only bottom border) */
7 | basic?: boolean;
8 |
9 | /** Initial value to display (uncontrolled mode) */
10 | defaultValue?: string;
11 |
12 | /** Disables input */
13 | disabled?: boolean;
14 |
15 | /** Fills width of parent container */
16 | fluid?: boolean;
17 |
18 | /** Callback invoked on value change */
19 | onchange?: (e: Event) => void;
20 |
21 | /** Input value */
22 | value?: string | number;
23 |
24 | [htmlAttrs: string]: any;
25 | }
26 |
27 | export class TextArea implements m.Component {
28 | public view({ attrs }: m.Vnode) {
29 | const {
30 | basic,
31 | class: className,
32 | disabled,
33 | fluid,
34 | intent,
35 | size,
36 | style,
37 | ...htmlAttrs
38 | } = attrs;
39 |
40 | const classes = classnames(
41 | Classes.INPUT,
42 | Classes.TEXT_AREA,
43 | basic && Classes.BASIC,
44 | disabled && Classes.DISABLED,
45 | fluid && Classes.FLUID,
46 | intent && `cui-${intent}`,
47 | size && `cui-${size}`,
48 | className
49 | );
50 |
51 | return m('', { class: classes, style }, m('textarea', { ...htmlAttrs }));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/text-area/text-area.md:
--------------------------------------------------------------------------------
1 | @# Text Area
2 | A user text area input.
3 |
4 | @example TextAreaExample
5 |
6 | @## Text Area Attrs
7 | @interface ITextAreaAttrs
8 |
--------------------------------------------------------------------------------
/src/components/toast/examples/declarative.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Button, Toaster, Toast, Switch } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/toast/examples/declarative.ts';
6 |
7 | interface INotification {
8 | key: number;
9 | message: string;
10 | }
11 |
12 | export class ToastDeclarativeExample {
13 | private notifications: INotification[] = [];
14 | private timeout: number = 3000;
15 |
16 | public view() {
17 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
18 | m(Button, {
19 | label: 'Show toast',
20 | intent: 'primary',
21 | onclick: this.show
22 | }),
23 |
24 | m(Toaster, {
25 | toasts: this.notifications.map(notification => m(Toast, {
26 | key: notification.key,
27 | message: notification.message,
28 | onDismiss: this.dismiss,
29 | timeout: this.timeout
30 | }))
31 | })
32 | ]);
33 | }
34 |
35 | private show = () => {
36 | this.notifications.push({
37 | message: 'Toast message',
38 | key: Date.now()
39 | });
40 | };
41 |
42 | private dismiss = (key: number) => {
43 | const index = this.notifications.findIndex(x => x.key === key);
44 | this.notifications.splice(index, 1);
45 | };
46 |
47 | private renderOptions() {
48 | return [
49 | m(Switch, {
50 | checked: this.timeout === 0,
51 | label: 'Timeout = 0',
52 | onchange: () => this.timeout = this.timeout === 0 ? 3000 : 0
53 | })
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/toast/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './declarative';
2 | export * from './default';
3 |
--------------------------------------------------------------------------------
/src/components/toast/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Toast';
2 | export * from './Toaster';
3 |
--------------------------------------------------------------------------------
/src/components/toast/toast.md:
--------------------------------------------------------------------------------
1 | @# Toast
2 | A toast notifies the user of an action in response to app events.
3 |
4 | @example ToastDefaultExample
5 |
6 | @## Declarative usage
7 | A toaster can be used declaratively by passing an array of `Toast` items to the `toasts` attribute. Note: the `onDimiss` callback must be provided and correctly handled in order to "dismiss" a toast.
8 | @example ToastDeclarativeExample
9 |
10 | @## Toaster Attrs
11 | @interface IToasterAttrs
12 |
13 | @## Toaster static methods (imperative mode)
14 | @methods IToaster
15 |
16 | @## Toast Attrs
17 | @interface IToastAttrs
18 |
--------------------------------------------------------------------------------
/src/components/tooltip/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Tooltip, PopoverPosition, Size, Switch, Button } from '@/';
3 | import { Example, PopoverPositionSelect, SizeSelect } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'components/tooltip/examples/index.ts';
6 |
7 | export class TooltipExample {
8 | private hasArrow: boolean = true;
9 | private position: PopoverPosition = 'bottom';
10 | private size: Size;
11 |
12 | public view() {
13 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
14 | m(Tooltip, {
15 | content: 'Tooltip content',
16 | position: this.position,
17 | hasArrow: this.hasArrow,
18 | size: this.size,
19 | trigger: m(Button, {
20 | label: 'Hover me'
21 | })
22 | })
23 | ]);
24 | }
25 |
26 | private renderOptions() {
27 | return [
28 | m('h5', 'Position'),
29 | m(PopoverPositionSelect, { onSelect: (position: PopoverPosition) => this.position = position }),
30 | m('h5', 'Size'),
31 | m(SizeSelect, { onSelect: (size: Size) => this.size = size }),
32 | m(Switch, {
33 | label: 'Has arrow',
34 | checked: this.hasArrow,
35 | onchange: () => this.hasArrow = !this.hasArrow
36 | })
37 | ];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/tooltip/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @import '../../_shared/mixins';
4 | @import '../../_shared/variables';
5 |
6 |
7 | @mixin cui-tooltip-sizing($padding, $font-size) {
8 | $arrow-size: floor(math.div($padding, 1.2));
9 | $arrow-offset: floor($arrow-size * 0.5);
10 | $font-size: floor(math.div($font-size, 1.05));
11 |
12 | .cui-popover-arrow {
13 | width: $arrow-size;
14 | height: $arrow-size;
15 |
16 | &::before {
17 | width: $arrow-size;
18 | height: $arrow-size;
19 | }
20 | }
21 |
22 | .cui-popover-content {
23 | padding: floor(math.div($padding, 1.5));
24 | font-size: $font-size;
25 | }
26 |
27 | &[data-popper-placement^="top"] .cui-popover-arrow {
28 | bottom: -$arrow-offset;
29 | }
30 |
31 | &[data-popper-placement^="bottom"] .cui-popover-arrow {
32 | top: -$arrow-offset;
33 | }
34 |
35 | &[data-popper-placement^="right"] .cui-popover-arrow {
36 | left: -$arrow-offset;
37 | }
38 |
39 | &[data-popper-placement^="left"] .cui-popover-arrow {
40 | right: -$arrow-offset;
41 | }
42 | }
43 |
44 | .cui-tooltip {
45 | @include cui-tooltip-sizing($cui-base-padding, $cui-font-size);
46 |
47 | .cui-popover-content {
48 | width: auto;
49 | min-width: auto;
50 | color: $cui-text-color;
51 | }
52 |
53 | @each $size in $cui-sizes {
54 | &.cui-#{$size} {
55 | @include cui-tooltip-sizing(map-get($cui-padding-map, $size),
56 | map-get($cui-font-size-map, $size))
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/tooltip/index.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import m from 'mithril';
3 | import { Classes, IAttrs, ISizeAttrs } from '../..';
4 | import { Popover, IPopoverAttrs, PopoverPosition } from '../popover';
5 |
6 | export interface ITooltipAttrs extends IAttrs, ISizeAttrs {
7 | /** Inner content */
8 | content?: m.Children;
9 |
10 | /** Content to trigger tooltip */
11 | trigger?: m.Child;
12 |
13 | /**
14 | * Position of content relative to trigger
15 | * @default 'auto'
16 | */
17 | position?: PopoverPosition;
18 |
19 | /**
20 | * Displays an arrow pointing to trigger
21 | * @default true
22 | */
23 | hasArrow?: boolean;
24 |
25 | /** Duration of close delay on hover interaction */
26 | hoverCloseDelay?: number;
27 |
28 | /** Duration of open delay on hover interaction */
29 | hoverOpenDelay?: number;
30 |
31 | /**
32 | * Transition duration
33 | * @default 300
34 | */
35 | transitionDuration?: number;
36 | }
37 |
38 | export class Tooltip implements m.Component {
39 | public view({ attrs }: m.Vnode) {
40 | const { size, class: className, ...otherAttrs } = attrs;
41 |
42 | const classes = classnames(
43 | Classes.TOOLTIP,
44 | size && `cui-${size}`,
45 | className
46 | );
47 |
48 | return m(Popover, {
49 | addToStack: false,
50 | triggerActiveClass: '',
51 | ...otherAttrs as IPopoverAttrs,
52 | class: classes,
53 | interactionType: 'hover-trigger'
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/tooltip/tooltip.md:
--------------------------------------------------------------------------------
1 | @# Tooltip
2 | A tooltip displays a simple hover container in relation to the `trigger` element. It is composed of a `Popover` component and can only be triggered via hover events.
3 |
4 | @example TooltipExample
5 |
6 | @## Tooltip Attrs
7 | @interface ITooltipAttrs
8 |
--------------------------------------------------------------------------------
/src/components/tree/Tree.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import classnames from 'classnames';
3 | import { Classes, IAttrs } from '../../_shared';
4 |
5 | export interface ITreeAttrs extends IAttrs {
6 | /** An array of child nodes */
7 | nodes?: m.Vnode[];
8 |
9 | [htmlAttrs: string]: any;
10 | }
11 |
12 | export class Tree implements m.Component {
13 | public view({ attrs }: m.Vnode) {
14 | const { nodes, class: className, ...htmlAttrs } = attrs;
15 | const treeClasses = classnames(Classes.TREE, className);
16 |
17 | return m('ul', {
18 | ...htmlAttrs,
19 | class: treeClasses
20 | }, nodes);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/tree/examples/data.ts:
--------------------------------------------------------------------------------
1 | export interface IModel {
2 | id: string;
3 | type: string;
4 | children: IModel[];
5 | }
6 |
7 | export const data = [
8 | {
9 | id: '0',
10 | type: 'Folder',
11 | children: [
12 | {
13 | id: '0-0',
14 | type: 'Folder',
15 | children: [
16 | {
17 | id: '0-0-0',
18 | type: 'File',
19 | children: []
20 | }
21 | ]
22 | }
23 | ]
24 | },
25 | {
26 | id: '1',
27 | type: 'Folder',
28 | children: [
29 | {
30 | id: '1-0',
31 | type: 'File',
32 | children: []
33 | }
34 | ]
35 | },
36 | {
37 | id: '2',
38 | type: 'Folder',
39 | children: [
40 | {
41 | id: '2-0',
42 | type: 'File',
43 | children: []
44 | },
45 | {
46 | id: '2-1',
47 | type: 'File',
48 | children: []
49 | }
50 | ]
51 | }
52 | ] as IModel[];
53 |
--------------------------------------------------------------------------------
/src/components/tree/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/mixins';
2 | @import '../../_shared/variables';
3 |
4 | .cui-tree {
5 | margin: 0;
6 | padding: 0;
7 | list-style: none;
8 | }
9 |
10 | .cui-tree-node-list {
11 | margin:0;
12 | padding-left: 20px;
13 | list-style:none;
14 | }
15 |
16 | .cui-tree-node-content {
17 | display: flex;
18 | align-items: center;
19 | padding: $cui-base-size * 0.5;
20 |
21 | &:hover {
22 | background-color: $cui-base-bg-color-hover;
23 | }
24 | }
25 |
26 | .cui-tree-node-caret,
27 | .cui-tree-node-caret-none {
28 | text-align:center;
29 | position:relative;
30 | }
31 |
32 | .cui-tree-node-caret-none {
33 | opacity:0;
34 | visibility: hidden;
35 | }
36 |
37 | .cui-tree-node-caret {
38 | color: $cui-text-color;
39 | display: inline-block;
40 | transition: transform ($cui-transition-duration * 2) $cui-transition-ease;
41 | transform-origin: center center;
42 | cursor:pointer;
43 | margin-right: 5px;
44 |
45 | &.cui-tree-node-caret-open {
46 | transform: rotate(90deg);
47 | transform-origin: center center;
48 | }
49 | }
50 |
51 | .cui-tree-node-label {
52 | @include cui-overflow-ellipsis();
53 |
54 | flex: 1 1 auto;
55 | position:relative;
56 | user-select:none;
57 | font-size: $cui-font-size;
58 |
59 | span {
60 | display:inline;
61 | }
62 | }
63 |
64 | .cui-tree-node-content-right {
65 | display:flex;
66 | margin-left: 5px;
67 | }
68 |
69 | .cui-tree-node-content-left {
70 | margin-right: 5px;
71 | display:flex;
72 | }
73 |
74 | .cui-tree-node.cui-tree-node-selected {
75 | > .cui-tree-node-content {
76 | background: $blue-grey50;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/tree/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Tree';
2 | export * from './TreeNode';
3 |
--------------------------------------------------------------------------------
/src/components/tree/tree.md:
--------------------------------------------------------------------------------
1 | @# Tree
2 | A tree component displays expandable and selectable tree hierarchies.
3 |
4 | @example TreeExample
5 |
6 | @## Tree Attrs
7 | @interface ITreeAttrs
8 |
9 | @## TreeNode Attrs
10 | @interface ITreeNodeAttrs
11 |
--------------------------------------------------------------------------------
/src/core/_nav.md:
--------------------------------------------------------------------------------
1 | @page introduction
2 | @page utils
3 | @page components
4 |
--------------------------------------------------------------------------------
/src/core/colors.md:
--------------------------------------------------------------------------------
1 | @# Colors
2 |
3 | Construct-ui provides a set of colors based on the 2014 Material Design color spec.
4 |
5 | Colors can be used in your JS code like so:
6 |
7 | ```javascript
8 | import { Colors } from 'construct-ui';
9 |
10 | m('', { style: { color: Colors.RED100 }})
11 | ```
12 |
13 | Sass variables can also be used:
14 |
15 | @import '~construct-ui/lib/scss/variables';
16 |
17 | .red {
18 | color: $red100
19 | }
20 |
21 |
22 | @example ColorsExample
23 |
--------------------------------------------------------------------------------
/src/core/examples/colors.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import tinycolor from 'tinycolor2';
3 | import { Colors, Grid, Col, getObjectKeys } from '@/';
4 | import { Color } from '@shared/colors';
5 |
6 | const colors = getObjectKeys(Colors);
7 |
8 | const colorGroups = colors
9 | .filter(color => !color.includes('WHITE'))
10 | .reduce((colorGroup, color, index) => {
11 | const groupIndex = Math.floor(index / 10);
12 |
13 | if (!colorGroup[groupIndex]) {
14 | colorGroup[groupIndex] = [];
15 | }
16 |
17 | colorGroup[groupIndex].push(color);
18 |
19 | return colorGroup;
20 | }, [] as any);
21 |
22 | const greyScaleColors = colorGroups.slice(0, 2);
23 | const coreColors = colorGroups.slice(2, colorGroups.length);
24 |
25 | export class ColorsExample {
26 | public view() {
27 | return m('.cui-example-colors', [
28 | m('h2', 'Greyscale Colors'),
29 | this.renderColorGrid(greyScaleColors),
30 |
31 | m('h2', 'Core colors'),
32 | this.renderColorGrid(coreColors)
33 | ]);
34 | }
35 |
36 | private renderColorGrid(groups: Color[][]) {
37 | const colSpan = {
38 | xs: 12,
39 | md: 6,
40 | sm: 6,
41 | lg: 4
42 | };
43 |
44 | return m(Grid, { gutter: 20 }, groups.map(colorGroup => [
45 | m(Col, { span: colSpan }, [
46 | m('.cui-example-colors-group', colorGroup.map(color => this.renderColorbar(color)))
47 | ])
48 | ]));
49 | }
50 |
51 | private renderColorbar(color: Color) {
52 | const style = {
53 | background: Colors[color],
54 | color: tinycolor(Colors[color]).isLight() ? 'black' : 'white'
55 | };
56 |
57 | return m('.cui-example-colors-bar', { style }, [
58 | m('', color),
59 | m('', Colors[color])
60 | ]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/core/examples/icons.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Col, Grid, Icon, Icons, Card, Input, getObjectKeys } from '@/';
3 |
4 | const iconNames = getObjectKeys(Icons).slice(1);
5 |
6 | export class IconsExample {
7 | private searchQuery: string = '';
8 |
9 | public view() {
10 | const icons = this.getFilteredIcons();
11 |
12 | const searchInput = m(Input, {
13 | autofocus: true,
14 | contentLeft: m(Icon, { name: Icons.SEARCH }),
15 | contentRight: this.searchQuery ? m(Icon, {
16 | name: Icons.X,
17 | onclick: () => this.searchQuery = ''
18 | }) : undefined,
19 | fluid: true,
20 | placeholder: 'Search icons...',
21 | oninput: (e: Event) => this.searchQuery = (e.target as HTMLInputElement).value,
22 | style: 'margin-bottom: 30px',
23 | value: this.searchQuery
24 | });
25 |
26 | const cols = icons.map(iconName => m(Col, { span: 4 }, [
27 | m(Card, { class: 'cui-example-icon-card', interactive: true }, [
28 | m(Icon, {
29 | name: Icons[iconName],
30 | size: 'xl'
31 | }),
32 | m('span', Icons[iconName])
33 | ])
34 | ]));
35 |
36 | return [
37 | searchInput,
38 | m(Grid, { gutter: 10 }, cols)
39 | ];
40 | }
41 |
42 | private getFilteredIcons() {
43 | if (!this.searchQuery) {
44 | return iconNames;
45 | }
46 |
47 | const query = this.searchQuery.toLowerCase();
48 |
49 | return iconNames.filter(iconName => Icons[iconName].toLowerCase().includes(query));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/core/examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './colors';
2 | export * from './icons';
3 |
--------------------------------------------------------------------------------
/src/core/getting-started.md:
--------------------------------------------------------------------------------
1 | @# Getting Started
2 | Construct-ui is a [Mithril.js](https://github.com/MithrilJS/mithril.js) UI library
3 |
4 | ## Installation
5 | ```sh
6 | npm i --save construct-ui
7 | ```
8 |
9 | ## Usage
10 | ```javascript
11 | import { Button, Icons } from 'construct-ui';
12 |
13 | const page = {
14 | view() {
15 | return m(Button, {
16 | iconLeft: Icons.FILTER,
17 | intent: 'primary',
18 | label: 'Button',
19 | size: 'xl'
20 | });
21 | }
22 | }
23 |
24 | m.mount(document.body, page)
25 | ```
26 |
27 | Include the main CSS file:
28 |
29 | ```javascript
30 | // Use with a bundler like webpack or parcel
31 | import 'path/to/node_modules/construct-ui/lib/index.css'
32 | ```
33 |
34 | ## Browser Support
35 | + Chrome >= 54
36 | + Firefox >= 52
37 | + Edge >= 14
38 | + Safari 9+
39 |
--------------------------------------------------------------------------------
/src/core/icons.md:
--------------------------------------------------------------------------------
1 | @# Icons
2 | Construct-ui includes a set of icons provided by the Feather Icons library. For usage, see the `Icon` component.
3 |
4 | @example IconsExample
5 |
--------------------------------------------------------------------------------
/src/core/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | reference: introduction
3 | title: 'Introduction'
4 | ---
5 |
6 | @page getting-started
7 | @page typography
8 | @page colors
9 | @page icons
10 |
--------------------------------------------------------------------------------
/src/core/typography.scss:
--------------------------------------------------------------------------------
1 | @import '../_shared/mixins';
2 | @import '../_shared/variables';
3 |
4 | // Headings
5 |
6 | h1, h2, h3, h4, h5, h6,
7 | .cui-h1, .cui-h2, .cui-h3, .cui-h4, .cui-h5, .cui-h6 {
8 | margin: 0;
9 | margin-bottom: $cui-headings-margin-bottom;
10 | font-family: $cui-headings-font-family;
11 | font-weight: $cui-headings-font-weight;
12 | line-height: $cui-headings-line-height;
13 | color: $cui-headings-color;
14 | }
15 |
16 | h1, .cui-h1 { font-size: $cui-h1-font-size; }
17 | h2, .cui-h2 { font-size: $cui-h2-font-size; }
18 | h3, .cui-h3 { font-size: $cui-h3-font-size; }
19 | h4, .cui-h4 { font-size: $cui-h4-font-size; }
20 | h5, .cui-h5 { font-size: $cui-h5-font-size; }
21 | h6, .cui-h6 { font-size: $cui-h6-font-size; }
22 |
23 | // Paragraph
24 |
25 | p {
26 | margin: 0;
27 | margin-bottom: $cui-spacer;
28 | }
29 |
30 | // Text
31 |
32 | .cui-text-muted {
33 | color: $cui-text-muted;
34 | }
35 |
36 | .cui-text-disabled {
37 | color: $cui-text-disabled;
38 | }
39 |
40 | // Code
41 |
42 | code {
43 | padding: 2.5px 5px;
44 | color: $blue-grey800;
45 | background: white;
46 | border-radius: 5px;
47 | font-size: $cui-font-size-sm;
48 | border:solid 1px $blue-grey100;
49 | word-break: break-word;
50 | }
51 |
52 | pre {
53 | border-radius: 5px;
54 | background: white;
55 | border:solid 1px $blue-grey100;
56 | }
57 |
58 | pre code {
59 | font-size: inherit;
60 | color: inherit;
61 | word-break: normal;
62 | background: none;
63 | border: none;
64 | padding: 15px 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import './utils/focus-manager/index.scss';
2 | @import './core/reset.scss';
3 | @import './core/typography.scss';
4 | @import './components/index.scss';
5 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './_shared';
2 | export * from './components';
3 | export * from './utils';
4 |
--------------------------------------------------------------------------------
/src/utils/focus-manager/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Button, Switch, Input, FocusManager } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'utils/focus-manager/examples/index.ts';
6 |
7 | export class FocusManagerExample {
8 | private showFocusOnTab = true;
9 |
10 | public onremove() {
11 | FocusManager.showFocusOnlyOnTab();
12 | }
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), direction: 'column', src: EXAMPLE_SRC }, [
16 | m(Button, { label: 'Button', style: 'margin-bottom: 20px' }),
17 |
18 | m(Input, { placeholder: 'Placeholder...' })
19 | ]);
20 | }
21 |
22 | private renderOptions() {
23 | return [
24 | m(Switch, {
25 | checked: this.showFocusOnTab,
26 | label: 'Show focus only on tab',
27 | onchange: () => {
28 | this.showFocusOnTab
29 | ? FocusManager.alwaysShowFocus()
30 | : FocusManager.showFocusOnlyOnTab();
31 | this.showFocusOnTab = !this.showFocusOnTab;
32 | }
33 | })
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/focus-manager/focus-manager.md:
--------------------------------------------------------------------------------
1 | @# Focus Manager
2 | A utility class for managing focus states. Note: must be explicitly initialized, preferably somewhere in your app root file.
3 |
4 | ```javascript
5 | import { FocusManager } from 'construct-ui';
6 |
7 | FocusManager.showFocusOnlyOnTab();
8 | ```
9 |
10 | @example FocusManagerExample
11 |
12 | @## API
13 | @methods FocusManager
14 |
--------------------------------------------------------------------------------
/src/utils/focus-manager/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../_shared/variables';
2 | @import '../../_shared/mixins';
3 |
4 | :focus {
5 | @include focus-outline();
6 | }
7 |
8 | .cui-focus-disabled :focus {
9 | outline: none !important;
10 |
11 | ~ .cui-control-indicator {
12 | outline: none !important;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/focus-manager/index.ts:
--------------------------------------------------------------------------------
1 | import { Keys, Classes } from '../../_shared';
2 |
3 | class FocusManager {
4 | /** Focus outline is shown only when tabbing through elements */
5 | public showFocusOnlyOnTab() {
6 | const body = document.body;
7 |
8 | body.addEventListener('mousedown', this.handleMouseDown);
9 | body.addEventListener('keydown', this.handleKeyDown);
10 | }
11 |
12 | /** Focus outline is always shown (mouse click and tab) */
13 | public alwaysShowFocus() {
14 | const body = document.body;
15 |
16 | body.removeEventListener('mousedown', this.handleMouseDown);
17 | body.removeEventListener('keydown', this.handleKeyDown);
18 | body.classList.remove(Classes.FOCUS_DISABLED);
19 | }
20 |
21 | private handleMouseDown = () => {
22 | document.body.classList.add(Classes.FOCUS_DISABLED);
23 | };
24 |
25 | private handleKeyDown = (e: KeyboardEvent) => {
26 | if (e.which === Keys.TAB) {
27 | document.body.classList.remove(Classes.FOCUS_DISABLED);
28 | }
29 | };
30 | }
31 |
32 | export default new FocusManager();
33 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as FocusManager } from './focus-manager';
2 | export { default as ResponsiveManager } from './responsive-manager';
3 | export { default as TransitionManager } from './transition-manager';
4 |
--------------------------------------------------------------------------------
/src/utils/responsive-manager/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { ResponsiveManager } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'utils/responsive-manager/examples/index.ts';
6 |
7 | export class ResponsiveManagerExample {
8 | public view() {
9 | return m(Example, { center: false, src: EXAMPLE_SRC }, [
10 | m('', [
11 | 'Current screen size = ',
12 | ResponsiveManager.is('xs') && 'XS',
13 | ResponsiveManager.is('sm') && 'SM',
14 | ResponsiveManager.is('md') && 'MD',
15 | ResponsiveManager.is('lg') && 'LG',
16 | ResponsiveManager.is('xl') && 'XL'
17 | ])
18 | ]);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils/responsive-manager/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { Breakpoint, Breakpoints, getObjectKeys } from '../../_shared';
3 | // @ts-ignore
4 | import ssm from 'simplestatemanager/dist/ssm.min.js';
5 |
6 | const breakpointKeys = getObjectKeys(Breakpoints);
7 |
8 | class ResponsiveManager {
9 | /** Key value of active breakpoints */
10 | public activeBreakpoints: Record;
11 |
12 | /** Binds breakpoints */
13 | public initialize(breakpoints: Record = Breakpoints) {
14 | this.destroy();
15 |
16 | breakpointKeys.map(key => ssm.addState({
17 | id: key,
18 | query: breakpoints[key],
19 | onEnter: () => {
20 | this.activeBreakpoints = {
21 | ...this.activeBreakpoints,
22 | [key]: true
23 | };
24 | m.redraw();
25 | },
26 | onLeave: () => {
27 | this.activeBreakpoints = {
28 | ...this.activeBreakpoints,
29 | [key]: false
30 | };
31 | m.redraw();
32 | }
33 | }));
34 | }
35 |
36 | /** Checks if current breakpoint string is active */
37 | public is(key: keyof typeof Breakpoints) {
38 | return this.activeBreakpoints[key] === true;
39 | }
40 |
41 | /** Unbinds all breakpoints */
42 | public destroy() {
43 | ssm.removeStates(breakpointKeys);
44 | }
45 | }
46 |
47 | export default new ResponsiveManager();
48 |
--------------------------------------------------------------------------------
/src/utils/responsive-manager/responsive-manager.md:
--------------------------------------------------------------------------------
1 | @# Responsive Manager
2 | A utility class that provides a simple wrapper over the ssm library for detecting media query changes.
3 | The default breakpoints (shown below) can be overriden by calling `ResponsiveManager.initialize` with custom values for the different sizes.
4 |
5 | ```javascript
6 | import { ResponsiveManager } from 'construct-ui';
7 |
8 | ResponsiveManager.initialize({
9 | xs: '(max-width: 575.98px)',
10 | sm: '(min-width: 576px) and (max-width: 767.98px)',
11 | md: '(min-width: 768px) and (max-width: 991.98px)',
12 | lg: '(min-width: 992px) and (max-width: 1199.98px)',
13 | xl: '(min-width: 1200px)'
14 | });
15 | ```
16 |
17 | @example ResponsiveManagerExample
18 |
19 | @## API
20 | @interface ResponsiveManager
21 |
22 | @methods ResponsiveManager
23 |
--------------------------------------------------------------------------------
/src/utils/transition-manager/examples/index.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { TransitionManager, Switch, Button, Popover, Tooltip, Dialog, Classes, ButtonGroup } from '@/';
3 | import { Example } from '@shared/examples';
4 |
5 | const EXAMPLE_SRC = 'utils/transition-manager/examples/index.ts';
6 |
7 | export class TransitionManagerExample {
8 | private isDialogOpen = false;
9 |
10 | public onremove() {
11 | TransitionManager.isEnabled = true;
12 | }
13 |
14 | public view() {
15 | return m(Example, { options: this.renderOptions(), src: EXAMPLE_SRC }, [
16 | m(ButtonGroup, [
17 | m(Popover, {
18 | content: 'Content',
19 | trigger: m(Button, { label: 'Popover trigger' })
20 | }),
21 |
22 | m(Tooltip, {
23 | content: 'Content',
24 | trigger: m(Button, { label: 'Tooltip trigger' })
25 | }),
26 |
27 | m(Button, {
28 | label: 'Show dialog',
29 | onclick: () => this.isDialogOpen = !this.isDialogOpen
30 | })
31 | ]),
32 |
33 | m(Dialog, {
34 | content: 'Content',
35 | isOpen: this.isDialogOpen,
36 | onClose: this.closeDialog,
37 | title: 'Dialog title',
38 | footer: m(`.${Classes.ALIGN_RIGHT}`, [
39 | m(Button, {
40 | label: 'Close',
41 | onclick: this.closeDialog
42 | })
43 | ])
44 | })
45 | ]);
46 | }
47 |
48 | private closeDialog = () => this.isDialogOpen = false;
49 |
50 | private renderOptions() {
51 | return [
52 | m(Switch, {
53 | label: 'Enabled',
54 | checked: TransitionManager.isEnabled,
55 | onchange: () => {
56 | TransitionManager.isEnabled
57 | ? TransitionManager.disable()
58 | : TransitionManager.enable();
59 | }
60 | })
61 | ];
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/utils/transition-manager/index.ts:
--------------------------------------------------------------------------------
1 | class TransitionManager {
2 | /** Whether transitions are active */
3 | public isEnabled = true;
4 |
5 | /** Enable all transitions */
6 | public enable() {
7 | this.isEnabled = true;
8 | }
9 |
10 | /** Disable all transitions */
11 | public disable() {
12 | return this.isEnabled = false;
13 | }
14 | }
15 |
16 | export default new TransitionManager();
17 |
--------------------------------------------------------------------------------
/src/utils/transition-manager/transition-manager.md:
--------------------------------------------------------------------------------
1 | @# Transition Manager
2 | A simple utility class to enable/disable CSS open/close transitions. Components affected are: `Dialog`, `Popover`, and `Tooltip`. `TransitionManager.disable()` is equivalent to passing `transitionDuration=0`.
3 |
4 | ```javascript
5 | import { TransitionManager } from 'construct-ui';
6 | TransitionManager.disable();
7 | ```
8 |
9 | @example TransitionManagerExample
10 |
11 | @## API
12 | @interface TransitionManager
13 |
14 | @methods TransitionManager
15 |
--------------------------------------------------------------------------------
/src/utils/utils.md:
--------------------------------------------------------------------------------
1 | ---
2 | reference: utils
3 | title: 'Utils'
4 | ---
5 |
6 | @page focus-manager
7 | @page responsive-manager
8 | @page transition-manager
9 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | import matchMediaPolyfill from 'mq-polyfill';
2 |
3 | matchMediaPolyfill(window);
4 |
5 | window.resizeTo = function resizeTo(width, height) {
6 | Object.assign(this, {
7 | innerWidth: width,
8 | innerHeight: height,
9 | outerWidth: width,
10 | outerHeight: height
11 | }).dispatchEvent(new this.Event('resize'));
12 | };
13 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "../",
5 | "declaration": false,
6 | },
7 | "include": [
8 | "../src/**/*.spec.ts"
9 | ],
10 | "exclude": [
11 | "../node_modules"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/test/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { defineConfig } from 'vite';
3 | import checker from 'vite-plugin-checker';
4 | import tsconfigPaths from 'vite-tsconfig-paths';
5 |
6 | export default defineConfig({
7 | plugins: [
8 | tsconfigPaths(),
9 | checker({
10 | typescript: true
11 | })
12 | ],
13 | test: {
14 | include: ['../src/**/*.spec.ts'],
15 | environment: 'jsdom',
16 | globals: true,
17 | setupFiles: [resolve(__dirname, 'setup.ts')],
18 | reporters: 'dot'
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "./lib/cjs"
6 | },
7 | "exclude": [
8 | "**/*.spec.ts",
9 | "**/examples.ts",
10 | "**/examples/*.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "es2015",
5 | "outDir": "./lib/esm",
6 | },
7 | "exclude": [
8 | "**/*.spec.ts",
9 | "**/examples.ts",
10 | "**/examples/*.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "baseUrl": ".",
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "importHelpers": true,
8 | "lib": [
9 | "dom",
10 | "es7"
11 | ],
12 | "paths": {
13 | "@/": [
14 | "src/"
15 | ],
16 | "@shared/*": [
17 | "src/_shared/*"
18 | ],
19 | "@test-utils": [
20 | "src/_shared/test/utils"
21 | ]
22 | },
23 | "moduleResolution": "node",
24 | "noImplicitAny": true,
25 | "noUnusedLocals": true,
26 | "noUnusedParameters": true,
27 | "sourceMap": false,
28 | "strictNullChecks": true,
29 | "skipLibCheck": true,
30 | "target": "es5"
31 | },
32 | "include": [
33 | "./src"
34 | ],
35 | "exclude": [
36 | "node_modules"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | build: {
6 | lib: {
7 | entry: resolve(__dirname, 'src/index.ts'),
8 | name: 'CUI',
9 | formats: ['umd'],
10 | fileName: () => 'construct-ui.min.js'
11 | },
12 | outDir: 'lib',
13 | rollupOptions: {
14 | external: ['mithril'],
15 | output: {
16 | globals: {
17 | mithril: 'm'
18 | }
19 | }
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------