├── .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 | logo -------------------------------------------------------------------------------- /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 | Construct-UI logo

3 |

4 | 5 |

Construct-UI

6 | 7 |
8 | 9 | ### A [Mithril.js](https://github.com/MithrilJS/mithril.js) UI library. 10 | 11 | [![npm package](https://img.shields.io/npm/v/construct-ui/latest.svg)](https://www.npmjs.com/package/construct-ui) 12 | ![Tests](https://github.com/vrimar/construct-ui/actions/workflows/run-tests.yml/badge.svg) 13 | [![Gitter](https://img.shields.io/gitter/room/vrimar/construct-ui.svg)](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(`${IconContents[name]}`); 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 | --------------------------------------------------------------------------------