├── .husky ├── commit-msg └── pre-push ├── .browserslistrc ├── site ├── views │ ├── core │ │ ├── core.json │ │ └── core.njk │ ├── _data │ │ ├── site.json │ │ └── env.js │ ├── general │ │ ├── general.json │ │ └── getting-started.njk │ ├── examples │ │ ├── examples.json │ │ ├── example │ │ │ ├── example.json │ │ │ ├── image.njk │ │ │ ├── form.njk │ │ │ ├── playground.njk │ │ │ ├── text.njk │ │ │ ├── multiple-playgrounds.njk │ │ │ └── list.njk │ │ └── examples.njk │ ├── plugins │ │ ├── plugins.json │ │ ├── settings │ │ │ ├── settings.json │ │ │ ├── text-setting.njk │ │ │ ├── bool-setting.njk │ │ │ ├── select-setting.njk │ │ │ └── slider-setting.njk │ │ ├── snippets-list │ │ │ ├── snippets-list.json │ │ │ └── snippets-list.njk │ │ ├── snippets-title │ │ │ ├── snippets-title.json │ │ │ └── snippets-title.njk │ │ ├── copy.njk │ │ ├── note.njk │ │ ├── editor.njk │ │ ├── theme.njk │ │ ├── direction.njk │ │ ├── snippets.njk │ │ └── settings.njk │ ├── components │ │ ├── components.json │ │ └── components.njk │ ├── robots.njk │ ├── _layouts │ │ ├── html5.njk │ │ └── basic.njk │ ├── _includes │ │ ├── sidebar.njk │ │ ├── footer.njk │ │ └── header.njk │ ├── sitemap.njk │ └── index.njk ├── static │ └── assets │ │ ├── patrick.png │ │ ├── uip-logo.png │ │ ├── uip-logo.svg │ │ └── github.svg ├── postcss.config.cjs ├── src │ ├── common │ │ ├── code.less │ │ ├── typography.less │ │ └── code.dark.less │ ├── examples │ │ ├── form.less │ │ ├── image.less │ │ ├── multiple-playgrounds.less │ │ └── list.less │ ├── variables.less │ ├── playground.less │ ├── playground.ts │ └── page │ │ ├── sidebar.less │ │ ├── header.less │ │ └── page.less ├── tsconfig.json ├── e11y │ ├── open.postaction.js │ ├── markdown.shortcut.js │ ├── prismjs.lib.js │ └── markdown.lib.js ├── webpack.config.js ├── .eleventy.js └── package.json ├── .lintstagedrc.yml ├── docs └── images │ ├── uip-logo.png │ ├── UIPexample2.png │ ├── exadel-logo.png │ └── uip-logo.svg ├── src ├── plugins │ ├── note │ │ ├── note.less │ │ ├── README.md │ │ └── note.tsx │ ├── snippets-title │ │ ├── snippets-title.less │ │ ├── snippets-title.shape.ts │ │ ├── README.md │ │ └── snippets-title.tsx │ ├── settings │ │ ├── base-setting │ │ │ ├── base-setting.less │ │ │ └── base-setting.ts │ │ ├── all.less │ │ ├── text-setting │ │ │ ├── text-setting.less │ │ │ ├── README.md │ │ │ └── text-setting.tsx │ │ ├── bool-setting │ │ │ ├── bool-setting.less │ │ │ ├── README.md │ │ │ └── bool-setting.tsx │ │ ├── slider-setting │ │ │ ├── README.md │ │ │ ├── slider-setting.less │ │ │ └── slider-setting.tsx │ │ ├── select-setting │ │ │ ├── select-setting.less │ │ │ └── README.md │ │ ├── settings.icon.tsx │ │ ├── settings.less │ │ ├── README.md │ │ └── settings.tsx │ ├── reset │ │ ├── reset-button.less │ │ ├── reset-button.icon.tsx │ │ ├── reset-button.shape.ts │ │ └── reset-button.ts │ ├── snippets-list │ │ ├── snippets.icon.tsx │ │ ├── snippets-list.less │ │ ├── snippets-list.shape.ts │ │ ├── README.md │ │ └── snippets-list.tsx │ ├── copy │ │ ├── copy-button.icon.tsx │ │ ├── README.md │ │ ├── copy-button.shape.ts │ │ ├── copy-button.less │ │ └── copy-button.ts │ ├── registration.less │ ├── direction │ │ ├── dir-toggle.shape.ts │ │ ├── README.md │ │ ├── dir-toggle.less │ │ └── dir-toggle.tsx │ ├── theme │ │ ├── theme-toggle.shape.ts │ │ ├── theme-toggle.less │ │ ├── README.md │ │ ├── theme-toggle.tsx │ │ └── theme-toggle.icon.tsx │ ├── editor │ │ ├── editor.icon.tsx │ │ ├── README.md │ │ └── editor.less │ ├── snippets │ │ ├── README.md │ │ ├── snippets.less │ │ └── snippets.tsx │ └── registration.ts ├── core │ ├── base │ │ ├── source.ts │ │ ├── plugin.less │ │ ├── root.less │ │ ├── base.less │ │ ├── model.change.ts │ │ ├── plugin.ts │ │ ├── snippet.ts │ │ ├── state.storage.ts │ │ ├── root.ts │ │ └── README.md │ ├── registration.less │ ├── button │ │ ├── plugin-button.less │ │ └── plugin-button.ts │ ├── preview │ │ ├── README.md │ │ └── preview.less │ ├── registration.ts │ ├── panel │ │ ├── plugin-panel.less │ │ ├── plugin-panel.header.less │ │ ├── plugin-panel.vertical.less │ │ └── plugin-panel.horizontal.less │ ├── processors │ │ ├── rendering.ts │ │ ├── templates.ts │ │ ├── preprocessor.ts │ │ └── normalization.ts │ └── utils │ │ └── token-list.ts ├── registration.less ├── registration.all.less ├── registration.ts ├── registration.all.ts └── README.md ├── .stylelintrc.yml ├── .gitignore ├── .whitesource ├── tsdoc.json ├── .commitlintrc.yml ├── .gitattributes ├── .editorconfig ├── eslint.config.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── lint.yml │ ├── release.yml │ ├── cla.yml │ └── site.yaml └── dependabot.yml ├── tsconfig.json ├── LICENSE ├── .releaserc.yml ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── CLA.md /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitlint -e $1 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | >0.3% 2 | last 3 major version 3 | not dead 4 | -------------------------------------------------------------------------------- /site/views/core/core.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "core" 4 | } 5 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.ts": 2 | - "eslint --max-warnings 3" 3 | "*.less": 4 | - "stylelint" 5 | -------------------------------------------------------------------------------- /site/views/_data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://exadel-inc.github.io/ui-playground" 3 | } 4 | -------------------------------------------------------------------------------- /site/views/general/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "general" 4 | } 5 | -------------------------------------------------------------------------------- /site/views/examples/examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "examples" 4 | } 5 | -------------------------------------------------------------------------------- /site/views/plugins/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "plugins" 4 | } 5 | -------------------------------------------------------------------------------- /docs/images/uip-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exadel-inc/ui-playground/HEAD/docs/images/uip-logo.png -------------------------------------------------------------------------------- /site/views/components/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "components" 4 | } 5 | -------------------------------------------------------------------------------- /site/views/plugins/settings/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "setting" 4 | } 5 | -------------------------------------------------------------------------------- /site/views/examples/example/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "example" 4 | } 5 | -------------------------------------------------------------------------------- /docs/images/UIPexample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exadel-inc/ui-playground/HEAD/docs/images/UIPexample2.png -------------------------------------------------------------------------------- /docs/images/exadel-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exadel-inc/ui-playground/HEAD/docs/images/exadel-logo.png -------------------------------------------------------------------------------- /site/views/plugins/snippets-list/snippets-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "snippets" 4 | } 5 | -------------------------------------------------------------------------------- /site/static/assets/patrick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exadel-inc/ui-playground/HEAD/site/static/assets/patrick.png -------------------------------------------------------------------------------- /site/static/assets/uip-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exadel-inc/ui-playground/HEAD/site/static/assets/uip-logo.png -------------------------------------------------------------------------------- /site/views/general/getting-started.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | --- 4 | 5 | {% markdown 'src/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/plugins/snippets-title/snippets-title.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "basic.njk", 3 | "tags": "snippets" 4 | } 5 | -------------------------------------------------------------------------------- /src/plugins/note/note.less: -------------------------------------------------------------------------------- 1 | uip-note { 2 | grid-area: note; 3 | .uip-note-title { 4 | font-weight: 600; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/snippets-title/snippets-title.less: -------------------------------------------------------------------------------- 1 | .uip-snippets-title { 2 | font-size: 1.2rem; 3 | font-weight: 600; 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | # Use LESS preprocessor 2 | customSyntax: postcss-less 3 | 4 | extends: 5 | - '@exadel/stylelint-config-esl' 6 | -------------------------------------------------------------------------------- /site/views/plugins/copy.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Copy 3 | listItem: true 4 | --- 5 | 6 | {% markdown 'src/plugins/copy/README.md' %} 7 | -------------------------------------------------------------------------------- /site/views/plugins/note.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Note 3 | listItem: true 4 | --- 5 | 6 | {% markdown 'src/plugins/note/README.md' %} 7 | -------------------------------------------------------------------------------- /src/core/base/source.ts: -------------------------------------------------------------------------------- 1 | export type UIPEditableSource = 'js' | 'html'; 2 | 3 | export type UIPSource = UIPEditableSource | 'note'; 4 | -------------------------------------------------------------------------------- /site/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | ['postcss-preset-env', {autoprefixer: {grid: true}}] 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /site/views/plugins/editor.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Editor 3 | listItem: true 4 | --- 5 | 6 | {% markdown 'src/plugins/editor/README.md' %} 7 | -------------------------------------------------------------------------------- /site/views/plugins/theme.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Theme Toggle 3 | listItem: true 4 | --- 5 | 6 | {% markdown 'src/plugins/theme/README.md' %} 7 | -------------------------------------------------------------------------------- /src/registration.less: -------------------------------------------------------------------------------- 1 | @import './core/registration.less'; 2 | @import './plugins/registration.less'; 3 | @import './plugins/settings/all.less'; 4 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | # ignore lint-staged if it is merge commit 2 | git rev-parse -q --no-revs --verify MERGE_HEAD || npx lint-staged --verbose --quiet 3 | -------------------------------------------------------------------------------- /site/views/plugins/settings/text-setting.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Text Setting 3 | --- 4 | 5 | {% markdown 'src/plugins/settings/text-setting/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/plugins/snippets-list/snippets-list.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Snippets List 3 | --- 4 | 5 | {% markdown 'src/plugins/snippets-list/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/core/core.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core Elements 3 | --- 4 | 5 | {% markdown 'src/core/base/README.md' %} 6 | {% markdown 'src/core/preview/README.md' %} 7 | -------------------------------------------------------------------------------- /site/views/plugins/direction.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Text Direction Toggle 3 | listItem: true 4 | --- 5 | 6 | {% markdown 'src/plugins/direction/README.md' %} 7 | -------------------------------------------------------------------------------- /site/views/plugins/settings/bool-setting.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Boolean Setting 3 | --- 4 | 5 | {% markdown 'src/plugins/settings/bool-setting/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/plugins/snippets-title/snippets-title.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Snippets Title 3 | --- 4 | 5 | {% markdown 'src/plugins/snippets-title/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/plugins/settings/select-setting.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Select Setting 3 | --- 4 | 5 | {% markdown 'src/plugins/settings/select-setting/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/plugins/settings/slider-setting.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Slider Setting 3 | --- 4 | 5 | {% markdown 'src/plugins/settings/slider-setting/README.md' %} 6 | -------------------------------------------------------------------------------- /site/views/robots.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: "/robots.txt" 3 | eleventyExcludeFromCollections: true 4 | --- 5 | User-agent: * 6 | Sitemap: {{ site.url }}/sitemap.xml 7 | -------------------------------------------------------------------------------- /site/src/common/code.less: -------------------------------------------------------------------------------- 1 | pre { 2 | display: block; 3 | margin-top: 0; 4 | margin-bottom: 1rem; 5 | overflow: auto; 6 | } 7 | 8 | @import './code.dark.less'; 9 | -------------------------------------------------------------------------------- /site/views/plugins/snippets.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Snippets 3 | listItem: true 4 | isSnippetItems: true 5 | --- 6 | 7 | {% markdown 'src/plugins/snippets/README.md' %} 8 | -------------------------------------------------------------------------------- /site/views/plugins/settings.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: UIP Settings and Setting 3 | listItem: true 4 | isSettingList: true 5 | --- 6 | 7 | {% markdown 'src/plugins/settings/README.md' %} 8 | -------------------------------------------------------------------------------- /site/src/examples/form.less: -------------------------------------------------------------------------------- 1 | .demo-form { 2 | width: 160px; 3 | } 4 | 5 | .form-section { 6 | margin: 10px 0; 7 | } 8 | 9 | .validation-input:invalid { 10 | border: 2px solid red; 11 | } 12 | -------------------------------------------------------------------------------- /src/registration.all.less: -------------------------------------------------------------------------------- 1 | @import '../node_modules/@exadel/esl/modules/esl-alert/core.less'; 2 | @import '../node_modules/@exadel/esl/modules/esl-scrollbar/core.less'; 3 | 4 | @import './registration.less'; 5 | -------------------------------------------------------------------------------- /site/views/_data/env.js: -------------------------------------------------------------------------------- 1 | const env = process.argv.find(arg => arg.startsWith('--env='))?.split('=')[1]; 2 | module.exports = { 3 | isDev: env === 'development', 4 | version: process.env.npm_package_version 5 | }; 6 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../", 5 | "sourceMap": true, 6 | "module": "esnext" 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/core/registration.less: -------------------------------------------------------------------------------- 1 | @import './base/base.less'; 2 | @import './base/root.less'; 3 | @import './base/plugin.less'; 4 | 5 | @import './panel/plugin-panel.less'; 6 | @import './button/plugin-button.less'; 7 | 8 | @import './preview/preview.less'; 9 | -------------------------------------------------------------------------------- /src/plugins/settings/base-setting/base-setting.less: -------------------------------------------------------------------------------- 1 | .uip-setting { 2 | display: none; 3 | } 4 | .uip-settings-container .uip-setting { 5 | display: block; 6 | } 7 | 8 | .uip-setting:not(.uip-bool-setting) label { 9 | display: inline; 10 | } 11 | -------------------------------------------------------------------------------- /site/src/examples/image.less: -------------------------------------------------------------------------------- 1 | .img { 2 | &-64 { 3 | width: 64px; 4 | height: 64px; 5 | } 6 | 7 | &-128 { 8 | width: 128px; 9 | height: 128px; 10 | } 11 | 12 | &-256 { 13 | width: 256px; 14 | height: 256px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/settings/all.less: -------------------------------------------------------------------------------- 1 | @import 'base-setting/base-setting.less'; 2 | 3 | @import 'bool-setting/bool-setting.less'; 4 | @import 'text-setting/text-setting.less'; 5 | @import 'select-setting/select-setting.less'; 6 | @import 'slider-setting/slider-setting.less'; 7 | -------------------------------------------------------------------------------- /src/core/base/plugin.less: -------------------------------------------------------------------------------- 1 | .uip-plugin { 2 | &-inner { 3 | padding: 10px; 4 | } 5 | 6 | &-inner:empty { 7 | padding: 0; 8 | } 9 | 10 | &-inner-bg { 11 | color: var(--uip-plugin-fg); 12 | background: var(--uip-plugin-bg); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/plugins/reset/reset-button.less: -------------------------------------------------------------------------------- 1 | .uip-reset { 2 | display: inline-flex; 3 | cursor: pointer; 4 | 5 | > svg { 6 | fill: currentColor; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | &[disabled] { 12 | display: none; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .DS_Store 7 | 8 | # Libs 9 | node_modules 10 | 11 | # Generated surces 12 | /bundles 13 | /esm 14 | /site/dist 15 | 16 | # Logs & Tmp 17 | npm-debug.log 18 | 19 | # Report 20 | .report 21 | 22 | # Sonar 23 | /.scannerwork 24 | -------------------------------------------------------------------------------- /site/src/examples/multiple-playgrounds.less: -------------------------------------------------------------------------------- 1 | .component-section { 2 | margin-bottom: 40px; 3 | 4 | .title { 5 | margin-bottom: 15px; 6 | font-weight: 500; 7 | } 8 | 9 | .desc { 10 | font-size: 18px; 11 | } 12 | 13 | .uip-root { 14 | margin-top: 20px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/settings/text-setting/text-setting.less: -------------------------------------------------------------------------------- 1 | .uip-text-setting input { 2 | width: 100%; 3 | padding: 3px 1px 1px; 4 | font-size: 16px; 5 | color: var(--uip-plugin-fg, #000); 6 | background: transparent; 7 | border: 1px solid var(--uip-plugin-fg, #000); 8 | box-shadow: none; 9 | } 10 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": ["main"], 4 | "enableLicenseViolations": "true" 5 | }, 6 | "checkRunSettings": { 7 | "vulnerableCheckRunConclusionLevel": "success", 8 | "displayMode": "diff" 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "LOW" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "tagDefinitions": [ 4 | { 5 | "tagName": "@author", 6 | "syntaxKind": "block" 7 | }, 8 | { 9 | "tagName": "@implements", 10 | "syntaxKind": "modifier" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/snippets-list/snippets.icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | export const UIPSnippetsIcon = (): SVGElement => ( 4 | 5 | 6 | 7 | ) as SVGElement; 8 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | rules: 4 | header-max-length: [1, 'always', 120] 5 | body-max-length: [1, 'always', 150] 6 | body-max-line-length: [1, 'always', 150] 7 | footer-max-line-length: [1, 'always', 120] 8 | subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']] 9 | -------------------------------------------------------------------------------- /src/core/button/plugin-button.less: -------------------------------------------------------------------------------- 1 | .uip-plugin-button { 2 | position: relative; 3 | transition: color 0.25s ease-in-out; 4 | 5 | &::before { 6 | content: ''; 7 | position: absolute; 8 | inset: -4px; 9 | z-index: -1; 10 | } 11 | 12 | .uip-plugin-header-toolbar &:hover { 13 | color: var(--uip-plugin-hover-fg); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/registration.ts: -------------------------------------------------------------------------------- 1 | import {registerCore} from './core/registration'; 2 | import {registerPlugins, registerSettings} from './plugins/registration'; 3 | 4 | export * from './core/registration'; 5 | export * from './plugins/registration'; 6 | 7 | export function init(): void { 8 | registerCore(); 9 | registerPlugins(); 10 | registerSettings(); 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/snippets-list/snippets-list.less: -------------------------------------------------------------------------------- 1 | uip-snippets-list { 2 | display: block; 3 | 4 | button { 5 | color: inherit; 6 | border: none; 7 | margin: 0; 8 | background: none; 9 | appearance: none; 10 | font-size: inherit; 11 | font-weight: inherit; 12 | font-family: inherit; 13 | 14 | vertical-align: middle; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/copy/copy-button.icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | export const CopyIcon = (): SVGElement => ( 4 | 5 | 6 | 7 | ) as SVGElement; 8 | -------------------------------------------------------------------------------- /site/src/variables.less: -------------------------------------------------------------------------------- 1 | @landing-dark-bg: #4e4e4e; 2 | @section-border: #ebebeb; 3 | 4 | @dark-text: #1f1f1f; 5 | 6 | @light-color: #ffffff; 7 | @blue-color: #0082ca; 8 | @purple-color: #7630ea; 9 | 10 | @header-height-desktop: 84px; 11 | @header-height-mobile: 56px; 12 | 13 | @mobile: e('screen and (max-width: 767px)'); 14 | @tablet: e('screen and (min-width: 768px) and (max-width: 991px)'); 15 | -------------------------------------------------------------------------------- /site/views/examples/examples.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | --- 4 | 5 | {% if title %} 6 |

7 | {{ title | safe }} 8 |

9 | {% endif %} 10 | 17 | -------------------------------------------------------------------------------- /src/plugins/reset/reset-button.icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | export const ResetIcon = (): SVGElement => ( 4 | 5 | 6 | 7 | ) as SVGElement; 8 | -------------------------------------------------------------------------------- /site/views/_layouts/html5.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title or 'Home' }} 8 | {% block head %}{% endblock %} 9 | 10 | 11 | {% block body %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/plugins/snippets-list/snippets-list.shape.ts: -------------------------------------------------------------------------------- 1 | import type {UIPSnippetsList} from './snippets-list'; 2 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 3 | 4 | type UIPSnippetsShape = ESLBaseElementShape; 5 | 6 | declare global { 7 | namespace JSX { 8 | interface IntrinsicElements { 9 | 'uip-snippets-list': UIPSnippetsShape; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/snippets-title/snippets-title.shape.ts: -------------------------------------------------------------------------------- 1 | import type {UIPSnippetsTitle} from './snippets-title'; 2 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 3 | 4 | type UIPSnippetsShape = ESLBaseElementShape; 5 | 6 | declare global { 7 | namespace JSX { 8 | interface IntrinsicElements { 9 | 'uip-snippets-title': UIPSnippetsShape; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/copy/README.md: -------------------------------------------------------------------------------- 1 | # UIP Copy 2 | 3 | **UIPCopy** - button plugin to copy snippet to clipboard. 4 | 5 | The plugin can be added to the [UIP Editor](src/plugins/editor/README.md) toolbar header via attribute *copy*. 6 | **UIPCopy** also dispatches success alert message. 7 | 8 | ## Example 9 | ```html 10 | 11 | 12 | 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /src/plugins/settings/text-setting/README.md: -------------------------------------------------------------------------------- 1 | # UIP Text Setting 2 | 3 | **UIPTextSetting** - custom setting to input attribute's value. Extends [UIPSetting](src/plugins/settings/README.md) 4 | 5 | This setting represents a *text input* for changing attribute's value. 6 | 7 | ## Example 8 | 9 | ```html 10 | 11 | 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /site/src/playground.less: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | @import './common/typography'; 4 | @import './common/code'; 5 | @import './page/page.less'; 6 | 7 | @import './examples/form.less'; 8 | @import './examples/image.less'; 9 | @import './examples/list.less'; 10 | @import './examples/multiple-playgrounds.less'; 11 | 12 | @import (inline) '~prismjs/themes/prism.css'; 13 | 14 | @import (inline) '~@exadel/ui-playground/esm/registration.all.css'; 15 | -------------------------------------------------------------------------------- /src/plugins/registration.less: -------------------------------------------------------------------------------- 1 | @import './snippets-list/snippets-list.less'; 2 | @import './snippets-title/snippets-title.less'; 3 | 4 | @import './snippets/snippets.less'; 5 | @import './note/note.less'; 6 | 7 | @import './editor/editor.less'; 8 | @import './settings/settings.less'; 9 | 10 | @import './copy/copy-button.less'; 11 | @import './reset/reset-button.less'; 12 | @import './theme/theme-toggle.less'; 13 | @import './direction/dir-toggle.less'; 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Restrict LF ending 5 | # Windows users, Notepads is not accepted as your IDE for this repository! 6 | *.js text eol=lf 7 | *.ts text eol=lf 8 | *.jsx text eol=lf 9 | *.tsx text eol=lf 10 | *.json text eol=lf 11 | *.css text eol=lf 12 | *.less text eol=lf 13 | *.scss text eol=lf 14 | *.md text eol=lf 15 | *.html text eol=lf 16 | *.njk text eol=lf 17 | -------------------------------------------------------------------------------- /src/plugins/direction/dir-toggle.shape.ts: -------------------------------------------------------------------------------- 1 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 2 | import type {UIPDirSwitcher} from './dir-toggle'; 3 | 4 | export interface UIPDirSwitcherShape extends ESLBaseElementShape { 5 | children?: any; 6 | } 7 | 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | 'uip-dir-toggle': UIPDirSwitcherShape; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /site/views/_includes/sidebar.njk: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /src/plugins/theme/theme-toggle.shape.ts: -------------------------------------------------------------------------------- 1 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 2 | import type {UIPThemeSwitcher} from './theme-toggle'; 3 | 4 | export interface UIPThemeSwitcherShape extends ESLBaseElementShape { 5 | children?: any; 6 | } 7 | 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | 'uip-theme-toggle': UIPThemeSwitcherShape; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/core/preview/README.md: -------------------------------------------------------------------------------- 1 | # UIP Preview 2 | 3 | **UIPPreview** - is a mandatory UIP custom element that displays active markup. Extends [UIPPlugin](src/core/README.md#uip-plugin). 4 | 5 | **UIPPreview** element observes [UIPStateModel](src/core/README.md#uip-state-model) changes, but it doesn't produce them. 6 | 7 | The `uip-preview` should be placed inside [UIPRoot](src/core/README.md#uip-root) element. 8 | 9 | ## Example 10 | 11 | ```html 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /src/plugins/settings/bool-setting/bool-setting.less: -------------------------------------------------------------------------------- 1 | .uip-bool-setting { 2 | .label-field { 3 | display: inline-flex; 4 | flex-wrap: wrap; 5 | align-items: center; 6 | 7 | .label-input { 8 | margin: 5px 0 2px 6px; 9 | } 10 | 11 | .label-msg, 12 | .inconsistency-msg { 13 | margin-left: 6px; 14 | } 15 | } 16 | 17 | .inconsistency-msg { 18 | color: grey; 19 | 20 | &.disabled { 21 | font-size: 14px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # Project settings 9 | indent_style = space 10 | indent_size = 2 11 | ij_any_blank_lines_around_method = 0 12 | 13 | # Unchanged settings 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /src/plugins/editor/editor.icon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable-file max-len */ 2 | import React from 'jsx-dom'; 3 | 4 | export const EditorIcon = (): SVGElement => ( 5 | 6 | 7 | 8 | ) as SVGElement; 9 | -------------------------------------------------------------------------------- /src/plugins/snippets-title/README.md: -------------------------------------------------------------------------------- 1 | # UIP Snippets Title 2 | 3 | **UIPSnippetsTitle** - small plugin to display a title of the currently selected snippet. 4 | More details can be found in [UIP Snippets](src/plugins/snippets/README.md) section. 5 | 6 | The following sample will render snippets as a tab list in the header of the UI Playground: 7 | 8 | ## Example 9 | 10 | ```html 11 | 12 | 13 | ... 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /src/plugins/copy/copy-button.shape.ts: -------------------------------------------------------------------------------- 1 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 2 | import type {UIPCopy} from './copy-button'; 3 | import type {UIPEditableSource} from '../../core/base/source'; 4 | 5 | export interface UIPCopyShape extends ESLBaseElementShape { 6 | source?: UIPEditableSource; 7 | children?: any; 8 | } 9 | 10 | declare global { 11 | namespace JSX { 12 | interface IntrinsicElements { 13 | 'uip-copy': UIPCopyShape; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/theme/theme-toggle.less: -------------------------------------------------------------------------------- 1 | uip-theme-toggle { 2 | display: none; 3 | cursor: pointer; 4 | .uip-root &.uip-theme-toggle { 5 | display: inline-flex; 6 | } 7 | 8 | width: 1.5rem; 9 | height: 1.5rem; 10 | 11 | svg { 12 | width: 100%; 13 | height: 100%; 14 | 15 | fill: currentColor; 16 | stroke: currentColor; 17 | } 18 | 19 | &[theme='light'] > .uip-dark-theme { 20 | display: none; 21 | } 22 | &[theme='dark'] > .uip-light-theme { 23 | display: none; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugins/editor/README.md: -------------------------------------------------------------------------------- 1 | # UIP Editor 2 | 3 | **UIPEditor** - UIP module to provide live-editor for current UIPlayground content. 4 | Extends [UIPPlugin](src/core/README.md#uip-plugin). 5 | 6 | **UIPEditor** is based on [Codejar](https://medv.io/codejar/) editor. 7 | Use [Prism.js](https://prismjs.com/) for code highlighting by default. 8 | 9 | ## Example 10 | ```html 11 | 12 | 13 | 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /src/plugins/reset/reset-button.shape.ts: -------------------------------------------------------------------------------- 1 | import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core'; 2 | import type {UIPReset} from './reset-button'; 3 | import type {UIPEditableSource} from '../../core/base/source'; 4 | 5 | export interface UIPResetShape extends ESLBaseElementShape { 6 | source?: UIPEditableSource; 7 | children?: any; 8 | } 9 | 10 | declare global { 11 | namespace JSX { 12 | interface IntrinsicElements { 13 | 'uip-reset': UIPResetShape; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /site/e11y/open.postaction.js: -------------------------------------------------------------------------------- 1 | const {isDev} = require('../views/_data/env'); 2 | /** 3 | * Auto-open development server 4 | * Should be replaced with OOTB solution when https://github.com/11ty/eleventy-dev-server/issues/28 will be resolved 5 | */ 6 | module.exports = (config) => { 7 | if (!isDev) return; 8 | config.on('eleventy.after', async () => { 9 | const {port} = config.serverOptions; 10 | if (!port || global.hasOpened) return; 11 | await require('out-url').open(`http://localhost:${port}`); 12 | global.hasOpened = true; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/core/registration.ts: -------------------------------------------------------------------------------- 1 | import {UIPRoot} from './base/root'; 2 | import {UIPStateModel, ChangeAttrConfig} from './base/model'; 3 | import {UIPPlugin} from './base/plugin'; 4 | 5 | import {UIPPreview} from './preview/preview'; 6 | 7 | export {UIPRoot, UIPPlugin, UIPStateModel, UIPPreview, ChangeAttrConfig}; 8 | 9 | export * from './processors/normalization'; 10 | export * from './processors/rendering'; 11 | export * from './processors/templates'; 12 | 13 | export const registerCore = (): void => { 14 | UIPRoot.register(); 15 | UIPPreview.register(); 16 | }; 17 | -------------------------------------------------------------------------------- /src/registration.all.ts: -------------------------------------------------------------------------------- 1 | import {ESLToggleable} from '@exadel/esl/modules/esl-toggleable/core'; 2 | import {ESLTrigger} from '@exadel/esl/modules/esl-trigger/core'; 3 | import {ESLAlert} from '@exadel/esl/modules/esl-alert/core'; 4 | import {ESLScrollbar} from '@exadel/esl/modules/esl-scrollbar/core'; 5 | 6 | import {init} from './registration'; 7 | 8 | export * from './registration'; 9 | 10 | export function initWithESL(): void { 11 | ESLAlert.register(); 12 | ESLTrigger.register(); 13 | ESLToggleable.register(); 14 | ESLScrollbar.register(); 15 | 16 | init(); 17 | } 18 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {lang, strict} from '@exadel/eslint-config-esl'; 2 | import {recommended as eslRecommended} from '@exadel/eslint-plugin-esl'; 3 | 4 | export default [ 5 | { 6 | ignores: [ 7 | // Common configuration 8 | 'site/webpack.config.js', 9 | // Common directories 10 | 'node_modules/**', 11 | // Submodule output 12 | 'esm/**', 13 | 'bundles/**', 14 | 'site/dist/**', 15 | ] 16 | }, 17 | 18 | // Using shared ESL ESLint Config 19 | ...lang.ts, 20 | ...strict, 21 | 22 | // ESL ESLint Plugin 23 | ...eslRecommended 24 | ]; 25 | -------------------------------------------------------------------------------- /src/plugins/snippets-list/README.md: -------------------------------------------------------------------------------- 1 | # UIP Snippets List 2 | 3 | **UIPSnippetsList** - custom element to display a list of available snippets. 4 | 5 | **UIPSnippetsList** observes UIPModel snippets changes and updates the list of available snippets. 6 | Component supports active snippet item marker and snippet selection by click. 7 | More details can be found in [UIP Snippets](src/plugins/snippets/README.md) section. 8 | 9 | ## Example 10 | 11 | ```html 12 | 13 | 14 | 15 | ... 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /site/views/sitemap.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: "/sitemap.xml" 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | 8 | {% for page in collections.all %} 9 | 10 | {{ site.url }}{{ page.url.replace(r/\/$/, ".html") if page.data.tags else page.url }} 11 | {{ page.date.toISOString() }} 12 | {%- if page.url == "/" %} 13 | weekly 14 | 1.0 15 | {%- endif %} 16 | 17 | {% endfor %} 18 | 19 | -------------------------------------------------------------------------------- /site/views/_includes/footer.njk: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /site/e11y/markdown.shortcut.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fsAsync = require('fs').promises; 3 | 4 | const {markdown} = require('./markdown.lib'); 5 | 6 | class MDRenderer { 7 | 8 | /** Read file and render markdown */ 9 | static async renderFromFile(filePath) { 10 | const absolutePath = path.resolve(__dirname, '../../', filePath); 11 | const data = await fsAsync.readFile(absolutePath); 12 | const content = data.toString(); 13 | return markdown.render(content); 14 | } 15 | } 16 | 17 | module.exports = (config) => { 18 | config.addNunjucksAsyncShortcode('markdown', MDRenderer.renderFromFile); 19 | }; 20 | module.exports.MDRenderer = MDRenderer; 21 | -------------------------------------------------------------------------------- /src/plugins/settings/slider-setting/README.md: -------------------------------------------------------------------------------- 1 | # UIP Slider Setting 2 | 3 | **UIPSliderSetting** - custom setting to change attribute within a range of values. 4 | Extends [UIPSetting](src/plugins/settings/README.md). 5 | 6 | Setting behaves like a range input with a value displayed below. It has the following attributes: 7 | 8 | - **min** - minimum range value (default: 0). 9 | - **max** - maximum range value (default: 0). 10 | - **step** - step between range's values (default: 0). 11 | 12 | ## Example 13 | 14 | ```html 15 | 16 | 17 | 18 | ``` 19 | -------------------------------------------------------------------------------- /src/plugins/copy/copy-button.less: -------------------------------------------------------------------------------- 1 | uip-copy { 2 | display: none; 3 | } 4 | .uip-copy { 5 | display: inline-flex; 6 | cursor: pointer; 7 | 8 | > svg { 9 | fill: currentColor; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | } 14 | 15 | .uip-copy-alert { 16 | width: auto; 17 | max-width: 90vw; 18 | 19 | left: 50%; 20 | transform: translateX(-50%); 21 | 22 | text-align: center; 23 | padding: 0.5rem 1rem; 24 | margin-bottom: 2rem; 25 | 26 | border: 1px solid var(--uip-alert, #0097e7); 27 | border-radius: 0.25rem; 28 | color: var(--uip-alert, #0097e7); 29 | text-shadow: 0 0 1px rgb(0 75 116 / 25%); 30 | background-color: var(--uip-alert-bg, #d9effb); 31 | } 32 | -------------------------------------------------------------------------------- /site/src/playground.ts: -------------------------------------------------------------------------------- 1 | import {ESLSelect} from '@exadel/esl/modules/esl-forms/esl-select/core'; 2 | import {ESLImage} from '@exadel/esl/modules/esl-image/core'; 3 | import {ESLScrollbar} from '@exadel/esl/modules/esl-scrollbar/core'; 4 | import {ESLTrigger} from '@exadel/esl/modules/esl-trigger/core'; 5 | import {ESLToggleable} from '@exadel/esl/modules/esl-toggleable/core'; 6 | import {ESLAlert} from '@exadel/esl/modules/esl-alert/core'; 7 | 8 | ESLSelect.register(); 9 | ESLImage.register(); 10 | ESLScrollbar.register(); 11 | ESLTrigger.register(); 12 | ESLToggleable.register(); 13 | ESLAlert.register(); 14 | ESLAlert.init(); 15 | 16 | import {init} from '@exadel/ui-playground/esm/registration.js'; 17 | init(); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[\U0001F680]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /site/e11y/prismjs.lib.js: -------------------------------------------------------------------------------- 1 | const Prism = require('prismjs'); 2 | 3 | // Register highlighted languages 4 | require('prismjs/components/prism-bash'); 5 | require('prismjs/components/prism-css'); 6 | require('prismjs/components/prism-less'); 7 | require('prismjs/components/prism-javascript'); 8 | require('prismjs/components/prism-typescript'); 9 | 10 | const highlight = (str, lang) => { 11 | try { 12 | lang = lang || 'text'; 13 | if (!Prism.languages[lang]) return ``; 14 | return Prism.highlight(str, Prism.languages[lang], lang); 15 | } catch (e) { 16 | return ``; 17 | } 18 | }; 19 | 20 | module.exports = highlight; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "skipLibCheck": true, 5 | "experimentalDecorators": true, 6 | "allowJs": true, 7 | "alwaysStrict": true, 8 | "strictNullChecks": true, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true, 11 | "target": "ES6", 12 | "downlevelIteration": true, 13 | "module": "ES2020", 14 | "moduleResolution": "Node", 15 | "lib": [ 16 | "ES2015", 17 | "DOM", 18 | "DOM.Iterable" 19 | ], 20 | "rootDir": "src", 21 | "outDir": "./esm", 22 | "jsx": "react" 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | "exclude": [ 28 | "*.test.ts", 29 | "**/test/*.ts", 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/images/uip-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /site/static/assets/uip-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /site/e11y/markdown.lib.js: -------------------------------------------------------------------------------- 1 | const {isDev} = require('../views/_data/env'); 2 | const highlight = require('./prismjs.lib'); 3 | 4 | const MarkdownIt = require('markdown-it'); 5 | const replaceLink = require('markdown-it-replace-link'); 6 | 7 | const markdown = MarkdownIt({ 8 | html: true, 9 | highlight: highlight, 10 | replaceLink: function (link) { 11 | const domain = isDev ? 'http://localhost:3005/ui-playground' : 'https://exadel-inc.github.io/ui-playground'; 12 | // if link isn't external, then we replace it 13 | return !link.search(/^https?:\/\//) ? link : 14 | domain + link.replace(/(src)|(README.md)/g, '') 15 | } 16 | }).use(replaceLink); 17 | 18 | module.exports = (config) => { 19 | config.setLibrary('md', markdown); 20 | }; 21 | module.exports.markdown = markdown; 22 | -------------------------------------------------------------------------------- /src/plugins/note/README.md: -------------------------------------------------------------------------------- 1 | # UIP Note 2 | 3 | **UIPNote** - custom element that is associated with a snippet. **UIPNote** is displayed if the snippet item is active. 4 | 5 | ## Example 6 | 7 | ```html 8 | 9 |
10 | 11 |
12 | 15 | 18 | 21 | 24 | 25 | 26 | ... 27 |
28 | ``` 29 | -------------------------------------------------------------------------------- /src/core/panel/plugin-panel.less: -------------------------------------------------------------------------------- 1 | .uip-plugin-panel { 2 | display: flex; 3 | position: relative; 4 | 5 | &[resizing] .uip-plugin-inner, 6 | .no-animate & .uip-plugin-inner { 7 | transition: none !important; 8 | } 9 | 10 | .uip-plugin-resize-bar { 11 | border: 3px solid transparent; 12 | z-index: 10; 13 | margin: -3px; 14 | box-sizing: border-box; 15 | } 16 | 17 | &[collapsed] .uip-plugin-resize-bar, 18 | &:not([resizable]) .uip-plugin-resize-bar { 19 | display: none; 20 | } 21 | 22 | &[collapsed] .uip-plugin-inner { 23 | visibility: hidden; 24 | } 25 | 26 | --uip-plugin-width: 250px; 27 | --uip-plugin-height: 325px; 28 | } 29 | 30 | @import './plugin-panel.header.less'; 31 | @import './plugin-panel.horizontal.less'; 32 | @import './plugin-panel.vertical.less'; 33 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | #----------------------------- 2 | # CI Github Action: Lint 3 | #----------------------------- 4 | 5 | name: lint 6 | 7 | on: 8 | push: 9 | branches: [ main, main-beta ] 10 | pull_request: 11 | branches: [ main, main-beta ] 12 | workflow_dispatch: 13 | 14 | env: 15 | node-version: 20.x 16 | 17 | jobs: 18 | lint: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node v${{ env.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | cache: 'npm' 28 | node-version: ${{ env.node-version }} 29 | - name: Install NPM Dependencies 30 | run: npm ci 31 | - name: Run Linting JS 32 | run: npm run lint:js 33 | - name: Run Linting CSS 34 | run: npm run lint:css 35 | -------------------------------------------------------------------------------- /src/plugins/theme/README.md: -------------------------------------------------------------------------------- 1 | # UIP Theme Toggle 2 | 3 | **UIPThemeSwitcher** - theme switcher plugin for UIPlayground. 4 | 5 | The theme switcher can be added to the UIPlayground toolbar header or to the Settings toolbar 6 | via attribute *theme-toggle*. 7 | 8 | ## Example 1 9 | 10 | ```html 11 | 12 |
13 | 14 | 15 |
16 | ... 17 |
18 | ``` 19 | ## Example 2 20 | 21 | ```html 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /src/plugins/direction/README.md: -------------------------------------------------------------------------------- 1 | # UIP Text Direction Toggle 2 | 3 | **UIPDirSwitcher** - text direction switcher plugin for UIPlayground. 4 | 5 | The text direction switcher can be added to the UIPlayground toolbar header or to the Settings toolbar 6 | via attribute *dir-toggle*. 7 | 8 | ## Example 1 9 | 10 | ```html 11 | 12 |
13 | 14 | 15 |
16 | ... 17 |
18 | ``` 19 | ## Example 2 20 | 21 | ```html 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /site/src/common/typography.less: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | body { 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 5 | 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 6 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 7 | } 8 | 9 | p { 10 | margin-top: 0; 11 | margin-bottom: 1rem; 12 | } 13 | 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5 { 19 | margin-top: 0; 20 | margin-bottom: 0.5rem; 21 | font-weight: 500; 22 | line-height: 1.2; 23 | } 24 | 25 | h1 { 26 | font-size: 2.5rem; 27 | } 28 | 29 | ol, 30 | ul { 31 | margin-top: 0; 32 | margin-bottom: 1rem; 33 | 34 | ol, 35 | ul { 36 | margin-bottom: 0; 37 | } 38 | } 39 | 40 | li > ol, 41 | li > ul { 42 | margin-left: 1.1rem; 43 | } 44 | 45 | a { 46 | color: @blue-color; 47 | text-decoration: none; 48 | 49 | &:hover { 50 | opacity: 0.7; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /site/static/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/base/root.less: -------------------------------------------------------------------------------- 1 | uip-root { 2 | opacity: 0; 3 | transition: opacity 1s ease-in-out; 4 | } 5 | 6 | .uip-root { 7 | position: relative; 8 | display: grid; 9 | align-content: start; 10 | grid-template-columns: 1fr auto; 11 | grid-template-rows: auto minmax(0, auto); 12 | grid-template-areas: 13 | 'header header' 14 | 'note sidebar' 15 | 'preview sidebar' 16 | 'settings settings' 17 | 'editor editor' 18 | 'editor-js editor-js'; 19 | 20 | min-width: 100%; 21 | height: auto; 22 | opacity: 1; 23 | } 24 | 25 | .uip-toolbar { 26 | grid-area: header; 27 | position: relative; 28 | display: flex; 29 | justify-content: space-between; 30 | gap: 0.5em; 31 | align-items: center; 32 | width: 100%; 33 | margin: 0; 34 | padding: 5px; 35 | 36 | color: var(--uip-header-fg, #000); 37 | background: var(--uip-header-bg, transparent); 38 | border-bottom: var(--uip-header-divider, none); 39 | } 40 | -------------------------------------------------------------------------------- /site/src/examples/list.less: -------------------------------------------------------------------------------- 1 | .card { 2 | &-list { 3 | display: flex; 4 | gap: 20px; 5 | justify-content: center; 6 | } 7 | 8 | &-item { 9 | width: 200px; 10 | height: 200px; 11 | padding: 15px; 12 | text-align: center; 13 | overflow-wrap: break-word; 14 | 15 | &.red { 16 | background-color: red; 17 | color: white; 18 | } 19 | 20 | &.green { 21 | background-color: green; 22 | color: white; 23 | } 24 | 25 | &.aqua { 26 | background-color: aqua; 27 | } 28 | 29 | &.top { 30 | align-content: start; 31 | } 32 | &.center { 33 | align-content: center; 34 | } 35 | &.bottom { 36 | align-content: end; 37 | } 38 | } 39 | 40 | &-circle { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | border-radius: 50%; 45 | } 46 | } 47 | 48 | [wrap-items] { 49 | flex-wrap: wrap; 50 | } 51 | -------------------------------------------------------------------------------- /src/plugins/settings/select-setting/select-setting.less: -------------------------------------------------------------------------------- 1 | @import '@exadel/esl/modules/esl-forms/esl-select/core.less'; 2 | @import '@exadel/esl/modules/esl-forms/esl-select-list/core.less'; 3 | 4 | .uip-select-setting .esl-select { 5 | display: block; 6 | min-width: 0; 7 | 8 | .esl-select-renderer { 9 | padding: 0 2px; 10 | border: 1px solid var(--uip-plugin-divider, #ccc); 11 | 12 | &::after { 13 | color: var(--uip-plugin-fg, #000); 14 | } 15 | } 16 | } 17 | 18 | .uip-select-dropdown { 19 | .esl-select-list { 20 | color: var(--uip-plugin-fg, #000); 21 | background-color: var(--uip-plugin-bg, #fff); 22 | } 23 | 24 | .esl-select-list .esl-select-list-container { 25 | margin-right: 0; 26 | } 27 | 28 | .esl-select-item::before { 29 | border-color: var(--uip-plugin-fg, #000); 30 | } 31 | 32 | .esl-select-item[selected]::before { 33 | background-color: var(--uip-plugin-fg, #000); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /site/views/_includes/header.njk: -------------------------------------------------------------------------------- 1 |
2 | 17 |
18 | -------------------------------------------------------------------------------- /src/core/base/base.less: -------------------------------------------------------------------------------- 1 | // Default theme definition 2 | html { 3 | --uip-bg: #f5f2f0; 4 | --uip-fg: #000; 5 | 6 | --uip-header-fg: #000; 7 | --uip-header-bg: transparent; 8 | 9 | --uip-plugin-fg: #000; 10 | --uip-plugin-bg: #f5f2f0; 11 | --uip-plugin-hover-fg: #0086cc; 12 | --uip-plugin-accent: #0097e7; 13 | --uip-plugin-divider: #ccc; 14 | --uip-plugin-header-bg: #ccc; 15 | --uip-plugin-header-fg: #000; 16 | 17 | --uip-plugin-fg-v: #000; 18 | --uip-plugin-bg-v: transparent; 19 | 20 | --uip-alert: #0097e7; 21 | --uip-alert-bg: #d9effb; 22 | } 23 | 24 | // Default dark theme definition 25 | [dark-theme] { 26 | --uip-fg: #eee; 27 | --uip-bg: #3d3d3d; 28 | 29 | --uip-header-fg: #eee; 30 | --uip-header-bg: #2f2f2f; 31 | 32 | --uip-plugin-fg: #ccc; 33 | --uip-plugin-bg: #3d3d3d; 34 | --uip-plugin-divider: #ebebeb; 35 | --uip-plugin-divider-v: #ebebeb; 36 | --uip-plugin-header-bg: #2f2f2f; 37 | --uip-plugin-header-fg: #ccc; 38 | } 39 | -------------------------------------------------------------------------------- /site/src/page/sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | display: none; 3 | background-color: @landing-dark-bg; 4 | box-shadow: 4px 8px 8px 0 rgba(0, 0, 0, 0.5); 5 | position: fixed; 6 | top: @header-height-desktop; 7 | z-index: 10; 8 | max-width: 100vw; 9 | overflow: hidden; 10 | list-style: none; 11 | 12 | transition: 13 | max-width 0.5s ease-in-out, 14 | visibility 0.5s linear; 15 | 16 | &.open { 17 | transition: 18 | max-width 0.5s ease-in-out, 19 | visibility 0s linear; 20 | } 21 | 22 | @media @mobile { 23 | display: block; 24 | top: @header-height-mobile; 25 | height: calc(~'100vh - @{header-height-mobile}'); 26 | } 27 | 28 | .sidebar-nav { 29 | width: 20rem; 30 | height: 100%; 31 | padding: 20px 0; 32 | } 33 | 34 | .sidebar-nav-item { 35 | padding: 6px 25px; 36 | 37 | &-link { 38 | color: @light-color; 39 | } 40 | } 41 | } 42 | 43 | .sidebar:not([open]) { 44 | max-width: 0; 45 | visibility: hidden; 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[\U0001F41B]" 5 | labels: '' 6 | assignees: yadamskaya 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /site/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | entry: { 8 | 'playground': './src/playground.ts' 9 | }, 10 | resolve: { 11 | modules: ['../node_modules'], 12 | roots: [], 13 | extensions: ['.ts', '.js', '.tsx'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(tsx?)$/, 19 | loader: 'ts-loader', 20 | options: { 21 | compilerOptions: { 22 | target: 'ES6', 23 | declaration: true 24 | }, 25 | } 26 | },] 27 | }, 28 | output: { 29 | path: path.resolve(__dirname, 'dist/bundles'), 30 | filename: '[name].js' 31 | }, 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: 'static', 35 | openAnalyzer: false, 36 | generateStatsFile: true, 37 | statsFilename: 'dev.stats.json', 38 | reportFilename: 'dev.report.html' 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /src/plugins/settings/slider-setting/slider-setting.less: -------------------------------------------------------------------------------- 1 | .uip-slider-setting { 2 | input { 3 | display: block; 4 | width: 100%; 5 | } 6 | 7 | input[type='range'] { 8 | -webkit-appearance: none; 9 | margin: 10px 0 5px; 10 | height: 10px; 11 | background: var(--uip-plugin-bg, #000); 12 | border: 1px solid var(--uip-plugin-fg, #ccc); 13 | border-radius: 5px; 14 | } 15 | 16 | input[type='range']::-webkit-slider-thumb { 17 | -webkit-appearance: none; 18 | height: 15px; 19 | width: 15px; 20 | border-radius: 50%; 21 | background: var(--uip-plugin-fg, #000); 22 | cursor: ew-resize; 23 | transition: background 0.1s linear; 24 | } 25 | 26 | input[type='range']::-webkit-slider-runnable-track { 27 | -webkit-appearance: none; 28 | box-shadow: none; 29 | border: none; 30 | background: transparent; 31 | } 32 | 33 | .slider-value { 34 | opacity: 0.8; 35 | color: var(--uip-plugin-fg, #000); 36 | 37 | &.disabled { 38 | font-size: 14px; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/editor/editor.less: -------------------------------------------------------------------------------- 1 | .uip-editor { 2 | grid-area: editor; 3 | 4 | &:is([source='js'], [source='javascript']) { 5 | grid-area: editor-js; 6 | } 7 | 8 | &-inner { 9 | padding: 1em; 10 | } 11 | 12 | &-header-copy, 13 | &-header-reset { 14 | position: relative; 15 | width: 25px; 16 | height: 25px; 17 | } 18 | 19 | &-container { 20 | color: inherit; 21 | } 22 | &-container &-code { 23 | margin: 0; 24 | padding: 0; 25 | color: inherit; 26 | background: none; 27 | } 28 | 29 | &-scrollbar { 30 | flex: 0 0 auto; 31 | order: -1; 32 | opacity: 0; 33 | transition: opacity 0.3s ease-in-out; 34 | } 35 | 36 | &:not([collapsed]) &-scrollbar:not([inactive]) { 37 | opacity: 1; 38 | transition-delay: 0.5s; 39 | } 40 | 41 | &:not(.vertical):last-child { 42 | margin-bottom: 0; 43 | } 44 | 45 | &.readonly &-title { 46 | &::after { 47 | content: ' (readonly)'; 48 | } 49 | &[data-label-readonly]::after { 50 | content: ' (' attr(data-label-readonly) ')'; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /site/views/_layouts/basic.njk: -------------------------------------------------------------------------------- 1 | {% extends './html5.njk' %} 2 | {% set scrollbar = true %} 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | {% block extra_head %}{% endblock %} 9 | {% endblock %} 10 | 11 | {% block body %} 12 | {% include 'header.njk' %} 13 | {% include 'sidebar.njk' %} 14 | 15 |
16 |
17 | {% if isLandingPage %} 18 | {{ content | safe or 'No content.' }} 19 | {% endif %} 20 | {% if not isLandingPage %} 21 |
22 | {{ content | safe or 'No content.' }} 23 |
24 | {% endif %} 25 |
26 | {% include 'footer.njk' %} 27 |
28 | 29 | 30 | {% block extra_body %}{% endblock %} 31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Exadel 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 | -------------------------------------------------------------------------------- /src/plugins/reset/reset-button.ts: -------------------------------------------------------------------------------- 1 | import './reset-button.shape'; 2 | 3 | import {listen, attr, boolAttr} from '@exadel/esl/modules/esl-utils/decorators'; 4 | 5 | import {UIPPluginButton} from '../../core/button/plugin-button'; 6 | 7 | import type {UIPRoot} from '../../core/base/root'; 8 | import type {UIPEditableSource} from '../../core/base/source'; 9 | 10 | /** Button-plugin to reset snippet to default settings */ 11 | export class UIPReset extends UIPPluginButton { 12 | public static override is = 'uip-reset'; 13 | 14 | @boolAttr() public disabled: boolean; 15 | 16 | /** Source type to copy (html | js) */ 17 | @attr({defaultValue: 'html'}) public source: UIPEditableSource; 18 | 19 | public override onAction(): void { 20 | this.$root?.storage!.resetState(this.source); 21 | } 22 | 23 | @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model}) 24 | protected _onModelChange(): void { 25 | if (!this.model || !this.model.activeSnippet) return; 26 | if (this.source === 'js') this.disabled = !this.model.isJSChanged(); 27 | if (this.source === 'html') this.disabled = !this.model.isHTMLChanged(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/processors/rendering.ts: -------------------------------------------------------------------------------- 1 | import {ESLRandomText} from '@exadel/esl/modules/esl-random-text/core'; 2 | import {UIPPreprocessorService} from './preprocessor'; 3 | 4 | /** 5 | * Pre-processor services for JS content. 6 | * Rendering preprocessors applied to content before rendering and does not affect editor's content 7 | */ 8 | export const UIPJSRenderingPreprocessors = new UIPPreprocessorService(); 9 | /** 10 | * Pre-processor services for HTML content. 11 | * Rendering preprocessors applied to content before rendering and does not affect editor's content 12 | */ 13 | export const UIPHTMLRenderingPreprocessors = new UIPPreprocessorService(); 14 | 15 | // Register default pre-processors 16 | 17 | UIPHTMLRenderingPreprocessors.addRegexReplacer('text', //g, (term, count) => { 18 | const length = count ? parseInt(count, 10) : 10; 19 | return ESLRandomText.generateText(length); 20 | }); 21 | 22 | UIPHTMLRenderingPreprocessors.addRegexReplacer('text-html', //g, (term, name, count) => { 23 | const length = count ? parseInt(count, 10) : 100; 24 | return ESLRandomText.generateTextHTML(100 * length); 25 | }); 26 | -------------------------------------------------------------------------------- /src/plugins/theme/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import './theme-toggle.shape'; 2 | import React from 'jsx-dom'; 3 | import {listen} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {UIPPluginButton} from '../../core/button/plugin-button'; 5 | import {ThemeToggleIcon} from './theme-toggle.icon'; 6 | 7 | /** Theme switcher button-plugin for UI Playground widget */ 8 | export class UIPThemeSwitcher extends UIPPluginButton { 9 | public static override is = 'uip-theme-toggle'; 10 | public static override defaultTitle = 'Switch theme'; 11 | 12 | protected override connectedCallback(): void { 13 | super.connectedCallback(); 14 | this._onThemeChange(); 15 | this.appendChild(); 16 | } 17 | 18 | protected override disconnectedCallback(): void { 19 | super.disconnectedCallback(); 20 | this.innerHTML = ''; 21 | } 22 | 23 | protected override onAction(): void { 24 | this.$root?.toggleAttribute('dark-theme'); 25 | } 26 | 27 | @listen({event: 'uip:theme:change', target: ($this: UIPThemeSwitcher) => $this.$root}) 28 | protected _onThemeChange(): void { 29 | this.$$attr('theme', this.$root?.hasAttribute('dark-theme') ? 'dark' : 'light'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for npm 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | versioning-strategy: increase 13 | open-pull-requests-limit: 10 14 | schedule: 15 | interval: "weekly" 16 | labels: 17 | - "dependencies" 18 | reviewers: 19 | - "yadamska" 20 | assignees: 21 | - "yadamska" 22 | groups: 23 | esl: 24 | patterns: 25 | - "@exadel/esl" 26 | - "@exadel/eslint-plugin-esl" 27 | - "@exadel/eslint-config-esl" 28 | typescript-eslint: 29 | patterns: 30 | - "@typescript-eslint/*" 31 | commit-lint: 32 | patterns: 33 | - "@commitlint/*" 34 | semantic-release: 35 | patterns: 36 | - "semantic-release" 37 | - "@semantic-release/*" 38 | -------------------------------------------------------------------------------- /src/core/utils/token-list.ts: -------------------------------------------------------------------------------- 1 | import {intersection} from '@exadel/esl/modules/esl-utils/misc/set'; 2 | 3 | /** Class for processing attribute's tokens */ 4 | export class TokenListUtils { 5 | /** 6 | * Divides string by whitespace regexp 7 | * @returns array of items or empty array 8 | */ 9 | static split(str: string | null): string[] { 10 | return str?.split(/\s+/) || []; 11 | } 12 | 13 | /** Creates new string by concatenating all passed elements */ 14 | static join(values: any[]): string { 15 | return values.join(' '); 16 | } 17 | 18 | /** Checks if all array elements are equal */ 19 | static isAllEqual(values: any[]): boolean { 20 | return values.every((val) => val === values[0]); 21 | } 22 | 23 | /** Checks if array contains all elements from subArray */ 24 | static contains(array: T[], subArray: T[]): boolean { 25 | return subArray.every((val) => array.includes(val)); 26 | } 27 | 28 | /** Removes all element appearances from array */ 29 | static remove(array: T[], element: T): T[] { 30 | return array.filter((el) => el !== element); 31 | } 32 | 33 | /** Returns intersection of all arrays */ 34 | static intersection = intersection; 35 | } 36 | -------------------------------------------------------------------------------- /src/plugins/settings/bool-setting/README.md: -------------------------------------------------------------------------------- 1 | # UIP Bool Setting 2 | 3 | **UIPBoolSetting** - custom setting to add/remove attributes or append values to an attribute. 4 | Extends [UIPSetting](src/plugins/settings/README.md). 5 | 6 | **UIPBoolSetting** represents a checkbox. It has a *value* attribute to add/remove this *value* 7 | from attributes. 8 | 9 | The setting can exist in two modes: **replace** and **append**. 10 | 11 | **replace** mode is used by default. There are two ways to interpret it: when there is a *value* attribute 12 | specified and when it's not. When it's not specified, the setting adds/removes attribute specified in *attribute*. 13 | If we define *value*, the setting sets *attribute* value to it. 14 | 15 | The idea of **append** mode is to add/discard *value* (must be specified for this mode) to the *attribute*. 16 | For example, it can be useful for adding css classes (see example below). 17 | 18 | ## Example 19 | 20 | ```html 21 | 22 | 23 | 24 | 25 | 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /src/core/base/model.change.ts: -------------------------------------------------------------------------------- 1 | import {overrideEvent} from '@exadel/esl/modules/esl-utils/dom'; 2 | 3 | import type {UIPRoot} from './root'; 4 | import type {UIPStateModel} from './model'; 5 | import type {UIPSource} from './source'; 6 | 7 | export type UIPChangeInfo = { 8 | modifier: object; 9 | type: UIPSource; 10 | force?: boolean; 11 | }; 12 | 13 | export class UIPChangeEvent extends Event { 14 | public override readonly target: UIPRoot; 15 | 16 | public constructor( 17 | type: string, 18 | target: UIPRoot, 19 | public readonly changes: UIPChangeInfo[] 20 | ) { 21 | super(type, {bubbles: false, cancelable: false}); 22 | overrideEvent(this, 'target', target); 23 | } 24 | 25 | public get model(): UIPStateModel { 26 | return this.target.model; 27 | } 28 | 29 | public get force(): boolean { 30 | return this.changes.some((change) => change.force); 31 | } 32 | 33 | public get jsChanges(): UIPChangeInfo[] { 34 | return this.changes.filter((change) => change.type === 'js'); 35 | } 36 | 37 | public get htmlChanges(): UIPChangeInfo[] { 38 | return this.changes.filter((change) => change.type === 'html'); 39 | } 40 | 41 | public isOnlyModifier(modifier: object): boolean { 42 | return this.changes.every((change) => change.modifier === modifier); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/processors/templates.ts: -------------------------------------------------------------------------------- 1 | import {format} from '@exadel/esl/modules/esl-utils/misc'; 2 | 3 | interface UIPRenderingTemplateParams { 4 | title?: string; 5 | script?: string; 6 | content: string; 7 | [additional: string]: string | number | boolean | undefined | null; 8 | } 9 | 10 | export class UIPRenderingTemplatesService { 11 | /** Template storage */ 12 | protected static templates = new Map(); 13 | 14 | /** Register template */ 15 | public static add(name: string, template: string): void { 16 | this.templates.set(name, template); 17 | } 18 | /** Get template */ 19 | public static get(name: string): string | undefined { 20 | return this.templates.get(name); 21 | } 22 | 23 | /** Render template */ 24 | public static render(name: string, params: UIPRenderingTemplateParams): string { 25 | const template = this.get(name); 26 | if (!template) return params.content; 27 | return format(template, params); 28 | } 29 | } 30 | 31 | UIPRenderingTemplatesService.add('default', ` 32 | 33 | 34 | {title} 35 | 36 | 37 | 38 | 39 |
{content}
40 | 41 | 42 | `); 43 | -------------------------------------------------------------------------------- /site/.eleventy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const color = require('kleur'); 3 | 4 | module.exports = config => { 5 | // Init all 11ty config modules 6 | const cfgFiles = fs.readdirSync('./e11y/'); 7 | for (const file of cfgFiles) { 8 | if (file.startsWith('_')) continue; 9 | try { 10 | console.info(color.blue(`Initializing module: ${file}`)); 11 | require('./e11y/' + file)(config); 12 | console.info(color.green(`Module ${file} initialized.`)); 13 | } catch (e) { 14 | console.error(color.red(`Module ${file} initialization failed`)); 15 | throw e; 16 | } 17 | } 18 | 19 | config.setServerOptions({ 20 | port: 3005, 21 | domDiff: true, 22 | liveReload: true, 23 | watch: [ 24 | 'dist/bundles/*.js', 25 | 'dist/bundles/*.css', 26 | 'dist/bundles/*.map', 27 | ] 28 | }) 29 | 30 | config.addWatchTarget('../src/**/*.md'); 31 | config.addPassthroughCopy({ 32 | 'static/assets': 'assets', 33 | '../static': '.' 34 | }); 35 | 36 | return { 37 | dir: { 38 | input: 'views', 39 | output: 'dist', 40 | layouts: '_layouts', 41 | }, 42 | dataTemplateEngine: 'njk', 43 | htmlTemplateEngine: 'njk', 44 | passthroughFileCopy: true, 45 | templateFormats: ['md', 'njk'], 46 | pathPrefix: '/ui-playground/' 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /site/views/components/components.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Components 3 | --- 4 | 5 | {% if title %} 6 |

7 | {{ title | safe }} 8 |

9 | {% endif %} 10 | 11 |
    12 | {% for plugin in collections.core %} 13 |
  • 14 | {{ plugin.data.title }} 15 |
  • 16 | {%- endfor %} 17 |

    Plugins

    18 | {% for plugin in collections.plugins %} 19 | {% if plugin.data.listItem %} 20 |
  • 21 | {{ plugin.data.title }} 22 |
  • 23 | {%- endif %} 24 | {% if plugin.data.isSettingList %} 25 | 32 | {%- endif %} 33 | {% if plugin.data.isSnippetItems %} 34 | 41 | {%- endif %} 42 | {%- endfor %} 43 |
44 | -------------------------------------------------------------------------------- /src/core/panel/plugin-panel.header.less: -------------------------------------------------------------------------------- 1 | .uip-plugin-header { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | 6 | flex: 0 0 auto; 7 | 8 | color: var(--uip-plugin-header-fg, #000); 9 | background: var(--uip-plugin-header-bg, transparent); 10 | 11 | font-weight: 500; 12 | 13 | .uip-plugin[label] &, 14 | .uip-plugin[collapsible] & { 15 | padding: 2px 10px; 16 | } 17 | 18 | &-icon { 19 | display: inline-flex; 20 | width: 1.25rem; 21 | height: 1.25rem; 22 | margin-inline-end: 5px; 23 | transform-origin: center center; 24 | justify-content: center; 25 | 26 | > svg { 27 | fill: currentColor; 28 | stroke: currentColor; 29 | } 30 | } 31 | 32 | &-title { 33 | margin-inline-end: auto; 34 | } 35 | 36 | &-title:not(:empty) { 37 | flex: 1 1 auto; 38 | padding: 5px; 39 | line-height: 1em; 40 | } 41 | 42 | &-trigger { 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | width: 100%; 47 | height: 100%; 48 | z-index: 1; 49 | 50 | cursor: pointer; 51 | background: transparent; 52 | border: none; 53 | box-shadow: none; 54 | appearance: none; 55 | } 56 | 57 | &-toolbar { 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | gap: 0.5rem; 62 | z-index: 2; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | #----------------------------- 2 | # CI Github Action: Release 3 | #----------------------------- 4 | 5 | name: Release 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | dryRun: 11 | type: boolean 12 | default: true 13 | required: false 14 | description: 'Run in dry-run mode (no actual release)' 15 | 16 | env: 17 | node-version: 20.x 18 | 19 | jobs: 20 | release: 21 | name: Release 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Use Node v${{ env.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | cache: 'npm' 33 | node-version: ${{ env.node-version }} 34 | 35 | - name: Install NPM Dependencies 36 | run: npm ci 37 | 38 | - name: Run Semantic Release in Dry Run mode 39 | if: ${{ inputs.dryRun }} 40 | env: 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 43 | run: npx semantic-release --dry-run 44 | 45 | - name: Run Semantic Release 46 | if: ${{ !inputs.dryRun }} 47 | env: 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 50 | run: npx semantic-release 51 | -------------------------------------------------------------------------------- /site/src/common/code.dark.less: -------------------------------------------------------------------------------- 1 | .uip-root.dark-theme .uip-editor { 2 | .uip-editor-code { 3 | text-shadow: none; 4 | } 5 | 6 | .token.block-comment, 7 | .token.cdata, 8 | .token.comment, 9 | .token.doctype, 10 | .token.prolog { 11 | color: #999; 12 | } 13 | 14 | .token.punctuation { 15 | color: #ccc; 16 | } 17 | 18 | .token.attr-name, 19 | .token.deleted, 20 | .token.namespace, 21 | .token.tag { 22 | color: #e2777a; 23 | } 24 | 25 | .token.function-name { 26 | color: #6196cc; 27 | } 28 | 29 | .token.boolean, 30 | .token.function, 31 | .token.number { 32 | color: #f08d49; 33 | } 34 | 35 | .token.class-name, 36 | .token.constant, 37 | .token.property, 38 | .token.symbol { 39 | color: #f8c555; 40 | } 41 | 42 | .token.atrule, 43 | .token.builtin, 44 | .token.important, 45 | .token.keyword, 46 | .token.selector { 47 | color: #cc99cd; 48 | } 49 | 50 | .token.attr-value, 51 | .token.char, 52 | .token.regex, 53 | .token.string, 54 | .token.variable { 55 | color: #7ec699; 56 | } 57 | 58 | .token.entity, 59 | .token.operator, 60 | .token.url { 61 | color: #67cdcc; 62 | } 63 | 64 | .token.bold, 65 | .token.important { 66 | font-weight: 700; 67 | } 68 | 69 | .token.italic { 70 | font-style: italic; 71 | } 72 | 73 | .token.entity { 74 | cursor: help; 75 | } 76 | 77 | .token.inserted { 78 | color: green; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/plugins/snippets-title/snippets-title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | 5 | import {UIPPlugin} from '../../core/base/plugin'; 6 | import {UIPSnippets} from '../snippets/snippets'; 7 | 8 | import './snippets-title.shape'; 9 | import type {UIPRoot} from '../../core/base/root'; 10 | 11 | /** Common lightweight plugin to display currently selected snippet title */ 12 | export class UIPSnippetsTitle extends UIPPlugin { 13 | public static override is = 'uip-snippets-title'; 14 | 15 | @memoize() 16 | public override get $root(): UIPRoot | null { 17 | const parent: UIPSnippets = this.closest(`${UIPSnippets.is}`)!; 18 | if (parent) return parent.$root; 19 | return super.$root; 20 | } 21 | 22 | /** Active snippet title inner element */ 23 | @memoize() 24 | protected get $inner(): JSX.Element { 25 | const type = this.constructor as typeof UIPSnippetsTitle; 26 | return ; 27 | } 28 | 29 | protected override connectedCallback(): void { 30 | super.connectedCallback(); 31 | this.appendChild(this.$inner); 32 | setTimeout(() => this._onRootStateChange()); 33 | } 34 | 35 | /** Handles active snippet title change */ 36 | @listen({event: 'uip:snippet:change', target: ($this: UIPSnippetsTitle) => $this.$root}) 37 | protected _onRootStateChange(): void { 38 | this.$inner.textContent = this.model!.activeSnippet?.label || ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/plugins/theme/theme-toggle.icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | export const ThemeToggleIcon = (): SVGElement => (<> 4 | 5 | 6 | 7 | 8 | 9 | {/* eslint-disable-next-line max-len */} 10 | 11 | 12 | ) as SVGElement; 13 | -------------------------------------------------------------------------------- /src/plugins/settings/select-setting/README.md: -------------------------------------------------------------------------------- 1 | # UIP Select Setting 2 | 3 | **UIPSelectSetting** - custom setting to select attribute's value. Extends [UIPSetting](src/plugins/settings/README.md). 4 | 5 | **UIPSelectSetting** is rendered as [ESLSelect](https://github.com/exadel-inc/esl/tree/main/src/modules/esl-forms/esl-select) element. 6 | 7 | Select setting has two modes: **replace** and **append**. The first one (is used by default) replaces the attribute 8 | value with selected, and the second one appends selected value to the attribute. 9 | 10 | **UIPSelectSetting** also supports **multiple** attribute to allow selecting multiple values. 11 | 12 | ## Example 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | -------------------------------------------------------------------------------- /src/plugins/direction/dir-toggle.less: -------------------------------------------------------------------------------- 1 | uip-dir-toggle { 2 | display: none; 3 | } 4 | .uip-dir-toggle { 5 | display: inline-flex; 6 | gap: 0.25em; 7 | cursor: pointer; 8 | user-select: none; 9 | 10 | align-items: center; 11 | justify-content: center; 12 | 13 | height: 1.5rem; 14 | line-height: 1em; 15 | 16 | svg { 17 | width: 100%; 18 | height: 100%; 19 | 20 | fill: currentColor; 21 | stroke: currentColor; 22 | } 23 | 24 | &-label { 25 | display: none; 26 | } 27 | 28 | &-figure { 29 | display: flex; 30 | user-select: none; 31 | pointer-events: none; 32 | font-weight: 500; 33 | letter-spacing: -1px; 34 | margin: 0 0 0 -2px; 35 | align-items: center; 36 | } 37 | 38 | .uip-settings-container > & { 39 | display: flex; 40 | flex-direction: column; 41 | align-items: flex-start; 42 | width: fit-content; 43 | height: auto; 44 | } 45 | 46 | .uip-settings-container > & &-label { 47 | display: block; 48 | &::after { 49 | content: ':'; 50 | } 51 | } 52 | 53 | .uip-settings-container > & &-figure { 54 | width: auto; 55 | min-height: 1.5em; 56 | vertical-align: middle; 57 | 58 | border: 1px solid currentColor; 59 | border-radius: 0.25em; 60 | margin: 2px; 61 | padding: 2px; 62 | 63 | &::after { 64 | content: ''; 65 | display: block; 66 | width: 0.5em; 67 | height: 100%; 68 | background: var(--uip-plugin-divider); 69 | border-radius: inherit; 70 | margin-inline-start: 0.25em; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/plugins/note/note.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {UIPPlugin} from '../../core/base/plugin'; 5 | 6 | /** 7 | * Note {@link UIPPlugin} custom element definition 8 | * Container that is associated with a snippet item {@link UIPSnippetsTitle} 9 | */ 10 | export class UIPNote extends UIPPlugin { 11 | public static override is = 'uip-note'; 12 | public static title = 'Note: '; 13 | 14 | @memoize() 15 | protected get $inner(): HTMLElement { 16 | return (
) as HTMLElement; 17 | } 18 | 19 | @memoize() 20 | protected get $title(): HTMLElement { 21 | return (
{UIPNote.title}
) as HTMLElement; 22 | } 23 | 24 | protected override connectedCallback(): void { 25 | super.connectedCallback(); 26 | this.appendChild(this.$title); 27 | this.appendChild(this.$inner); 28 | } 29 | 30 | protected override disconnectedCallback(): void { 31 | this.removeChild(this.$inner); 32 | this.removeChild(this.$title); 33 | super.disconnectedCallback(); 34 | } 35 | 36 | /** Updates note content from the model state changes */ 37 | @listen({event: 'uip:change', target: ($this: UIPNote) => $this.$root}) 38 | protected _onRootStateChange(): void { 39 | this.writeContent(); 40 | } 41 | 42 | protected writeContent(): void { 43 | this.$title.textContent = UIPNote.title; 44 | this.$inner.innerHTML = this.model!.note || ''; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/snippets/README.md: -------------------------------------------------------------------------------- 1 | # UIP Snippets 2 | 3 | **UIPSnippets** - the primary plugin to display snippets of the UI Playground. 4 | 5 | **UIPSnippets** uses [UIPSnippetList](src/plugins/snippets-list/README.md) to render the list of snippets and [UIPSnippetsTitle](src/plugins/snippets-title/README.md) to render the title of the list. 6 | **UIPSnippets** can be rendered in two modes: *tabs* and *dropdown*. 7 | 8 | The following sample will render snippets as a tab list in the header: 9 | 10 | ```html 11 | 12 | 13 | ... 14 | 15 | ``` 16 | 17 | To render snippets as a dropdown list, set the `dropdown-view` attribute to `all`: 18 | 19 | ```html 20 | 21 | 22 | ... 23 | 24 | ``` 25 | 26 | The `dropdown-view` attribute can be any ESLMediaQuery value, so you can switch mode depending on the screen size. 27 | 28 | ```html 29 | 30 | 31 | ... 32 | 33 | ``` 34 | 35 | The class `uip-toolbar` is used to style the section as a toolbar-header for the UIPlayground. 36 | The combinations of `uip-snippets` and buttons (e.g. `uip-copy`, `uip-theme-toggle` or `uip-direction-toggle`) 37 | are also allowed with additional div wrapper: 38 | 39 | ```html 40 | 41 |
42 | 43 | 44 |
45 | ... 46 |
47 | ``` 48 | -------------------------------------------------------------------------------- /src/plugins/settings/settings.icon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable-file max-len */ 2 | import React from 'jsx-dom'; 3 | 4 | export const SettingsIcon = (): SVGElement => ( 5 | 6 | 7 | 8 | ) as SVGElement; 9 | -------------------------------------------------------------------------------- /src/plugins/copy/copy-button.ts: -------------------------------------------------------------------------------- 1 | import './copy-button.shape'; 2 | 3 | import {attr} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {UIPPluginButton} from '../../core/button/plugin-button'; 5 | 6 | import type {ESLAlertActionParams} from '@exadel/esl/modules/esl-alert/core'; 7 | import type {UIPEditableSource} from '../../core/base/source'; 8 | 9 | /** Button-plugin to copy snippet to clipboard */ 10 | export class UIPCopy extends UIPPluginButton { 11 | public static override is = 'uip-copy'; 12 | public static override defaultTitle = 'Copy to clipboard'; 13 | 14 | /** Source type to copy (html | js) */ 15 | @attr({defaultValue: 'html'}) public source: UIPEditableSource; 16 | 17 | public static msgConfig: ESLAlertActionParams = { 18 | text: 'Playground content copied to clipboard', 19 | cls: 'uip-copy-alert' 20 | }; 21 | 22 | /** Content to copy */ 23 | protected get content(): string | undefined { 24 | if (this.source === 'js' || this.source === 'html') return this.model?.[this.source]; 25 | } 26 | 27 | protected override connectedCallback(): void { 28 | if (!navigator.clipboard) this.hidden = true; 29 | super.connectedCallback(); 30 | } 31 | 32 | public override onAction(): void { 33 | this.copy().then(() => this.dispatchMessage()); 34 | } 35 | 36 | /** Dispatches success alert message */ 37 | protected dispatchMessage(): void { 38 | const detail = (this.constructor as typeof UIPCopy).msgConfig; 39 | this.$$fire('esl:alert:show', {detail}); 40 | } 41 | 42 | /** Copy model content to clipboard */ 43 | public copy(): Promise { 44 | return navigator.clipboard.writeText(this.content || ''); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | #----------------------------- 2 | # CI Github Action: CLA Assistant trigger script 3 | #----------------------------- 4 | 5 | name: cla-assistant 6 | 7 | on: 8 | issue_comment: # To detect standalone comment 9 | types: [created] 10 | pull_request_target: 11 | types: [opened,closed,synchronize] 12 | 13 | jobs: 14 | cla-assistant: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: cla-assistant-check 19 | uses: cla-assistant/github-action@v2.3.0 20 | if: startsWith(github.event.comment.body, 'recheckcla') || contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} 24 | with: 25 | path-to-signatures: 'signatures/cla.json' 26 | path-to-cla-document: 'https://github.com/exadel-inc/ui-playground/blob/main/CLA.md' 27 | 28 | branch: 'history/cla' 29 | allowlist: ala-n,yadamskaya,AlexanderBazukevich,nattallius,bot* 30 | 31 | signed-commit-message: 'chore: $contributorName has signed the CLA in #$pullRequestNo' 32 | create-file-commit-message: 'chore: created file for storing CLA Signatures' 33 | custom-notsigned-prcomment: 'Thank you for your submission, we really appreciate it. We ask that you sign our Contributor Licence Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.' 34 | -------------------------------------------------------------------------------- /src/core/base/plugin.ts: -------------------------------------------------------------------------------- 1 | import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core'; 2 | import {ESLTraversingQuery} from '@exadel/esl/modules/esl-traversing-query/core'; 3 | import {attr, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | 5 | import {UIPRoot} from './root'; 6 | 7 | import type {UIPStateModel} from './model'; 8 | 9 | /** 10 | * Base class for UI Playground plugins 11 | * Implements basic relation and styles 12 | */ 13 | export abstract class UIPPlugin extends ESLBaseElement { 14 | static readonly observedAttributes = ['root', 'label']; 15 | 16 | /** Visible label */ 17 | @attr() public label: string; 18 | 19 | /** Query for $root */ 20 | @attr({defaultValue: `::parent(${UIPRoot.is})`}) 21 | public root: string; 22 | 23 | /** Closest playground {@link UIPRoot} element */ 24 | @memoize() 25 | public get $root(): UIPRoot | null { 26 | return ESLTraversingQuery.first(this.root, this) as UIPRoot; 27 | } 28 | 29 | /** Returns {@link UIPStateModel} from $root instance */ 30 | protected get model(): UIPStateModel | null { 31 | return this.$root ? this.$root.model : null; 32 | } 33 | 34 | protected override connectedCallback(): void { 35 | super.connectedCallback(); 36 | this.classList.add('uip-plugin'); 37 | } 38 | 39 | protected override disconnectedCallback(): void { 40 | super.disconnectedCallback(); 41 | memoize.clear(this, '$root'); 42 | } 43 | 44 | protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { 45 | if (attrName === 'label') this.setAttribute('aria-label', newVal); 46 | if (attrName === 'root') memoize.clear(this, '$root'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/panel/plugin-panel.vertical.less: -------------------------------------------------------------------------------- 1 | .uip-plugin-panel.vertical.vertical { 2 | grid-area: sidebar; 3 | flex-direction: row; 4 | 5 | .uip-plugin-resize-bar { 6 | order: -1; 7 | cursor: col-resize; 8 | } 9 | 10 | .uip-plugin-inner { 11 | width: var(--uip-plugin-width, 250px); 12 | max-width: var(--uip-plugin-width, 250px); 13 | max-height: 100%; 14 | 15 | transition: 16 | visibility 0s linear, 17 | padding-left 0.3s linear, 18 | padding-right 0.3s linear, 19 | max-width 0.3s linear; 20 | } 21 | 22 | &[collapsed] .uip-plugin-inner { 23 | max-width: 0; 24 | padding-left: 0; 25 | padding-right: 0; 26 | 27 | transition: 28 | visibility 0.3s linear, 29 | padding-left 0.3s linear, 30 | padding-right 0.3s linear, 31 | max-width 0.3s linear; 32 | } 33 | 34 | .uip-plugin-header { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | padding: 2px 4px; 39 | } 40 | 41 | &[resizable] .uip-plugin-header { 42 | min-width: 1px; 43 | } 44 | 45 | .uip-plugin-header-toolbar { 46 | position: absolute; 47 | flex-direction: column; 48 | top: 0.25rem; 49 | left: 0; 50 | right: 0; 51 | gap: 0.5rem; 52 | } 53 | 54 | .uip-plugin-header-title { 55 | display: none; 56 | } 57 | &:not([collapsible]) { 58 | .uip-plugin-header.has-toolbar { 59 | min-width: calc(1.25rem + 8px); 60 | } 61 | .uip-plugin-header:not(.has-toolbar) { 62 | padding: 0; 63 | } 64 | .uip-plugin-header-icon { 65 | display: none; 66 | } 67 | } 68 | 69 | .uip-plugin-header-icon { 70 | width: 1.25rem; 71 | margin-inline-end: 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /site/views/examples/example/image.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example with image 3 | --- 4 | 5 |
6 | 7 | 8 |
9 | 12 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/core/processors/preprocessor.ts: -------------------------------------------------------------------------------- 1 | /** Pre-processor function, called to process content */ 2 | export type UIPTransformer = (input: string) => string; 3 | 4 | /** Rendering pre-processor service, that stores collection of {@link UIPRenderingPreprocessor}s */ 5 | export class UIPPreprocessorService { 6 | /** Pre-processor storage */ 7 | protected preprocessors: Record = {}; 8 | 9 | /** Add pre-processor {@link UIPRenderingPreprocessor} */ 10 | public add(name: string, preprocessor: UIPTransformer): void { 11 | this.preprocessors[name] = preprocessor; 12 | } 13 | 14 | /** Add pre-processor alias */ 15 | public addAlias(name: string, alias: string): void { 16 | this.preprocessors[name] = this.preprocessors[alias]; 17 | } 18 | 19 | /** Add pre-processor with RegExp replacer */ 20 | public addRegexReplacer(name: string, regex: RegExp, replaceValue: string): void; 21 | /** Add pre-processor with RegExp replacer */ 22 | public addRegexReplacer(name: string, regex: RegExp, replacer: (substring: string, ...args: any[]) => string): void; 23 | public addRegexReplacer(name: string, regex: RegExp, replacer: any): void { 24 | this.add(name, (input) => input.replace(regex, replacer)); 25 | } 26 | 27 | public get(name: string): UIPTransformer | undefined { 28 | return this.preprocessors[name]; 29 | } 30 | 31 | /** Pre-process html content */ 32 | public preprocess(html: string): string { 33 | return Object.keys(this.preprocessors) 34 | .map((name) => this.preprocessors[name]) 35 | .filter((preprocessor) => typeof preprocessor === 'function') 36 | .reduce((input, preprocessor) => preprocessor(input), html); 37 | } 38 | 39 | /** Clear all pre-processors */ 40 | public clear(): void { 41 | this.preprocessors = {}; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /site/views/examples/example/form.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example with form 3 | --- 4 | 5 | 6 | 7 | 8 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/plugins/registration.ts: -------------------------------------------------------------------------------- 1 | import {UIPSnippets} from './snippets/snippets'; 2 | import {UIPSnippetsList} from './snippets-list/snippets-list'; 3 | import {UIPSnippetsTitle} from './snippets-title/snippets-title'; 4 | export {UIPSnippetsTitle, UIPSnippetsList, UIPSnippets}; 5 | 6 | import {UIPEditor} from './editor/editor'; 7 | export {UIPEditor}; 8 | 9 | import {UIPSettings} from './settings/settings'; 10 | import {UIPSetting} from './settings/base-setting/base-setting'; 11 | import {UIPTextSetting} from './settings/text-setting/text-setting'; 12 | import {UIPSelectSetting} from './settings/select-setting/select-setting'; 13 | import {UIPBoolSetting} from './settings/bool-setting/bool-setting'; 14 | import {UIPSliderSetting} from './settings/slider-setting/slider-setting'; 15 | export {UIPSetting, UIPSettings, UIPTextSetting, UIPBoolSetting, UIPSelectSetting, UIPSliderSetting}; 16 | 17 | import {UIPCopy} from './copy/copy-button'; 18 | export {UIPCopy}; 19 | 20 | import {UIPReset} from './reset/reset-button'; 21 | export {UIPReset}; 22 | 23 | import {UIPNote} from './note/note'; 24 | export {UIPNote}; 25 | 26 | import {UIPDirSwitcher} from './direction/dir-toggle'; 27 | import {UIPThemeSwitcher} from './theme/theme-toggle'; 28 | export {UIPDirSwitcher, UIPThemeSwitcher}; 29 | 30 | export const registerSettings = (): void => { 31 | UIPSettings.register(); 32 | UIPBoolSetting.register(); 33 | UIPTextSetting.register(); 34 | UIPSelectSetting.register(); 35 | UIPSliderSetting.register(); 36 | }; 37 | 38 | export const registerPlugins = (): void => { 39 | UIPCopy.register(); 40 | UIPReset.register(); 41 | UIPDirSwitcher.register(); 42 | UIPThemeSwitcher.register(); 43 | 44 | UIPSnippets.register(); 45 | UIPSnippetsList.register(); 46 | UIPSnippetsTitle.register(); 47 | 48 | UIPEditor.register(); 49 | UIPNote.register(); 50 | }; 51 | -------------------------------------------------------------------------------- /src/plugins/settings/text-setting/text-setting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {attr, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | 5 | import {UIPSetting} from '../base-setting/base-setting'; 6 | import type {UIPStateModel} from '../../../core/base/model'; 7 | 8 | /** 9 | * Custom setting for inputting attribute's value 10 | */ 11 | export class UIPTextSetting extends UIPSetting { 12 | public static is = 'uip-text-setting'; 13 | 14 | /** Setting's visible name */ 15 | @attr({defaultValue: ''}) public label: string; 16 | 17 | /** Text input to change setting's value */ 18 | @memoize() 19 | protected get $field(): HTMLInputElement { 20 | return as HTMLInputElement; 21 | } 22 | 23 | protected override connectedCallback(): void { 24 | super.connectedCallback(); 25 | this.innerHTML = ''; 26 | 27 | const $inner = ; 31 | 32 | this.appendChild($inner); 33 | } 34 | 35 | updateFrom(model: UIPStateModel): void { 36 | super.updateFrom(model); 37 | const values = model.getAttribute(this.target, this.attribute); 38 | if (!values.length) { 39 | this.setInconsistency(this.NO_TARGET_MSG); 40 | } else { 41 | this.setValue(values[0]); 42 | } 43 | } 44 | 45 | protected getDisplayedValue(): string { 46 | return this.$field.value; 47 | } 48 | 49 | protected setValue(value: string | null): void { 50 | this.$field.value = value || ''; 51 | this.$field.placeholder = ''; 52 | } 53 | 54 | protected setInconsistency(msg = this.INCONSISTENT_VALUE_MSG): void { 55 | this.$field.value = ''; 56 | this.$field.placeholder = msg; 57 | } 58 | 59 | public setDisabled(force: boolean): void { 60 | this.$field.toggleAttribute('disabled', force); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/core/button/plugin-button.ts: -------------------------------------------------------------------------------- 1 | import {listen} from '@exadel/esl/modules/esl-utils/decorators'; 2 | import {ENTER, SPACE} from '@exadel/esl/modules/esl-utils/dom/keys'; 3 | 4 | import {UIPPlugin} from '../base/plugin'; 5 | 6 | /** 7 | * Base class for UI Playground plugin-buttons 8 | * Implements basic button behavior and accessibility 9 | */ 10 | export abstract class UIPPluginButton extends UIPPlugin { 11 | public static defaultTitle: string = ''; 12 | 13 | /** 14 | * Creates button element instance 15 | * @param content - inner content of created element 16 | * @param cls - class name of created element 17 | */ 18 | public static create( 19 | this: T, 20 | content?: string | Element | JSX.Element, 21 | cls: string = '' 22 | ): InstanceType { 23 | const $el = document.createElement(this.is) as InstanceType; 24 | $el.className = cls; 25 | if (typeof content === 'string') $el.innerHTML = content; 26 | if (typeof content === 'object') $el.appendChild(content); 27 | return $el; 28 | } 29 | 30 | /** Executes UIP action */ 31 | protected abstract onAction(): void; 32 | 33 | protected override connectedCallback(): void { 34 | super.connectedCallback(); 35 | this.$$cls('uip-plugin-button', true); 36 | this.setAttribute('tabindex', '0'); 37 | this.setAttribute('role', 'button'); 38 | 39 | const type = this.constructor as typeof UIPPluginButton; 40 | if (type.defaultTitle && !this.hasAttribute('title')) { 41 | this.setAttribute('title', this.label || type.defaultTitle); 42 | } 43 | } 44 | 45 | @listen('click') 46 | protected _onClick(e: PointerEvent): void { 47 | e.preventDefault(); 48 | this.onAction(); 49 | } 50 | 51 | @listen('keydown') 52 | protected _onKeyDown(e: KeyboardEvent): void { 53 | if (e.key === ENTER || e.key === SPACE) this.click(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/core/panel/plugin-panel.horizontal.less: -------------------------------------------------------------------------------- 1 | .uip-plugin-panel:not(.vertical) { 2 | flex-direction: column; 3 | margin-bottom: 1px; 4 | 5 | .uip-plugin-resize-bar { 6 | order: 100; 7 | cursor: row-resize; 8 | } 9 | 10 | .uip-plugin-inner { 11 | position: relative; 12 | display: grid; 13 | place-items: start; 14 | height: auto; 15 | // stylelint-disable-next-line declaration-block-no-duplicate-properties 16 | overflow: hidden; // Fallback 17 | overflow: clip; 18 | width: 100%; 19 | max-width: 100%; 20 | 21 | grid-template-rows: 1fr; 22 | transition: 23 | visibility 0s linear, 24 | padding-top 0.5s linear, 25 | padding-bottom 0.5s linear, 26 | grid-template-rows 0.5s ease-in-out; 27 | } 28 | 29 | .uip-plugin-content { 30 | max-height: var(--uip-plugin-height, 325px); 31 | } 32 | 33 | &[collapsed] .uip-plugin-inner { 34 | padding-top: 0; 35 | padding-bottom: 0; 36 | grid-template-rows: 0fr; 37 | transition: 38 | visibility 0.5s linear, 39 | padding-top 0.5s linear, 40 | padding-bottom 0.5s linear, 41 | grid-template-rows 0.5s ease-in-out; 42 | } 43 | 44 | .uip-plugin-header { 45 | padding: 5px 10px; 46 | width: 100%; 47 | } 48 | 49 | .uip-plugin-header-toolbar { 50 | gap: 0.75rem; 51 | margin-inline-end: 0.25rem; 52 | } 53 | 54 | &[collapsible] .uip-plugin-header::after { 55 | content: ''; 56 | justify-self: flex-end; 57 | position: relative; 58 | margin: 5px 10px; 59 | width: 15px; 60 | height: 15px; 61 | z-index: 1; 62 | background: currentColor; 63 | clip-path: polygon(50% 80%, 0% 30%, 5% 20%, 50% 65%, 95% 20%, 100% 30%); 64 | animation: rotate 0.5s linear both; 65 | } 66 | 67 | &[collapsible][collapsed] .uip-plugin-header::after { 68 | top: 2px; 69 | animation: rotate-back 0.5s linear both; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | # Stack trace is required 2 | debug: true 3 | 4 | # Tag format config 5 | tagFormat: "v${version}" 6 | 7 | # Repository 8 | repositoryUrl: "https://github.com/exadel-inc/ui-playground" 9 | 10 | # Branch release rules 11 | branches: 12 | - main 13 | - name: main-beta 14 | channel: beta 15 | prerelease: beta 16 | 17 | # Release configuration 18 | plugins: 19 | - - "@semantic-release/commit-analyzer" 20 | - preset: angular 21 | releaseRules: 22 | - type: style 23 | release: patch 24 | - type: refactor 25 | release: patch 26 | - type: fix 27 | message: "*MINOR VERSION*" 28 | release: minor 29 | - type: style 30 | message: "*MINOR VERSION*" 31 | release: minor 32 | - type: refactor 33 | message: "*MINOR VERSION*" 34 | release: minor 35 | - type: chore 36 | scope: deps 37 | release: patch 38 | - type: chore 39 | scope: patch 40 | release: patch 41 | 42 | - - "@semantic-release/release-notes-generator" 43 | - preset: angular 44 | parserOpts: 45 | noteKeywords: 46 | - BREAKING CHANGE 47 | - BREAKING CHANGES 48 | - BREAKING-CHANGE 49 | 50 | - - "@semantic-release/changelog" 51 | - changelogFile: CHANGELOG.md 52 | 53 | - - "@semantic-release/npm" 54 | - tarballDir: target 55 | 56 | - - "@semantic-release/git" 57 | - assets: 58 | - CHANGELOG.md 59 | - package.json 60 | - package-lock.json 61 | - site/package.json 62 | message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 63 | 64 | - - "@semantic-release/github" 65 | - assets: 66 | - path: target/*.tgz 67 | label: Released NPM Tarball 68 | - path: CHANGELOG.md 69 | label: Changelog 70 | successComment: false 71 | -------------------------------------------------------------------------------- /.github/workflows/site.yaml: -------------------------------------------------------------------------------- 1 | name: GH Pages Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | env: 10 | node-version: 20.x 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allows only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build-site: 27 | name: Build 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Setup Pages 33 | id: pages 34 | uses: actions/configure-pages@v4 35 | - name: Use Node v${{ env.node-version }} 36 | uses: actions/setup-node@v4 37 | with: 38 | cache: 'npm' 39 | node-version: ${{ env.node-version }} 40 | - name: Install NPM Dependencies 41 | run: npm ci 42 | - name: Build UIP 43 | run: npm run build 44 | - name: Build Site 45 | run: npm run build -w site 46 | env: 47 | SITE_BASE_URL: ${{ steps.pages.outputs.base_url }} 48 | BUILD_VERSION: ${{ github.run_number }} 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: ./site/dist 53 | 54 | # Deployment job 55 | deploy-site: 56 | name: Deploy 57 | environment: 58 | name: github-pages 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | runs-on: ubuntu-latest 61 | needs: build-site 62 | steps: 63 | - name: Deploy to GitHub Pages 64 | id: deployment 65 | uses: actions/deploy-pages@v4 66 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exadel/ui-playground-site", 3 | "version": "2.1.0-beta.10", 4 | "private": true, 5 | "description": "Site of UI Playground project", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">=20.0.0" 9 | }, 10 | "scripts": { 11 | "start": "npm run clean && npm rum watch", 12 | "clean": "rimraf dist", 13 | "build": "npm run clean && concurrently \"npm run build:ts\" \"npm run build:less && npm run build:minify\" && npm run build:eleventy", 14 | "build:ts": "webpack --mode=production", 15 | "build:minify": "cleancss -o dist/bundles/playground.css dist/bundles/playground.css", 16 | "build:less": "lessc --npm-import=\"prefix=~\" src/playground.less dist/bundles/playground.css --source-map=dist/bundles/playground.css.map", 17 | "build:eleventy": "npx @11ty/eleventy -- --env=production", 18 | "watch": "concurrently --kill-others \"npm run watch:lib\" \"npm run watch:eleventy\" \"npm run watch:ts\" \"npm run watch:less\"", 19 | "watch:lib": "cd .. && npm run watch", 20 | "watch:ts": "webpack --watch", 21 | "watch:less": "chokidar --initial \"../node_modules/@exadel/ui-playground/esm/**/*.less\" \"**/*.less\" -c \"npm run build:less\"", 22 | "watch:eleventy": "npx @11ty/eleventy --serve --port=3005 -- --env=development", 23 | "test": "echo \"Warn: no test declared for module\" && exit 0" 24 | }, 25 | "dependencies": { 26 | "@exadel/esl": "^5.0.0", 27 | "@exadel/ui-playground": "../", 28 | "@11ty/eleventy": "^2.0.1", 29 | "@11ty/eleventy-dev-server": "^2.0.6", 30 | "clean-css-cli": "^5.6.3", 31 | "kleur": "^4.1.5", 32 | "less-plugin-npm-import": "^2.1.0", 33 | "markdown-it": "^14.1.0", 34 | "markdown-it-replace-link": "^1.2.2", 35 | "out-url": "^1.2.2", 36 | "postcss": "^8.5.3", 37 | "postcss-preset-env": "^10.1.6", 38 | "ts-loader": "^9.5.2", 39 | "webpack": "^5.97.1", 40 | "webpack-bundle-analyzer": "^4.10.2", 41 | "webpack-cli": "^6.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/preview/preview.less: -------------------------------------------------------------------------------- 1 | .uip-preview { 2 | display: flex; 3 | overflow: hidden; 4 | grid-area: preview; 5 | 6 | direction: ltr; 7 | 8 | color: var(--uip-fg); 9 | accent-color: var(--uip-fg); 10 | background: var(--uip-bg); 11 | 12 | &-container { 13 | position: relative; 14 | display: grid; 15 | grid-template: 16 | 'uip-content v-scroll' 17 | 'h-scroll empty'; 18 | grid-template-columns: 1fr auto; 19 | grid-template-rows: 1fr auto; 20 | 21 | overflow: hidden; 22 | width: 100%; 23 | height: auto; 24 | max-width: 100%; 25 | max-height: 100%; 26 | 27 | min-height: 0; 28 | transition: min-height 0.5s linear; 29 | } 30 | 31 | &[resizable] &-container { 32 | resize: both; 33 | } 34 | 35 | &-inner { 36 | position: relative; 37 | grid-area: uip-content; 38 | max-width: 100%; 39 | max-height: 100%; 40 | 41 | opacity: 1; 42 | transition: opacity 0.2s linear; 43 | } 44 | 45 | &[loading] &-inner { 46 | opacity: 0; 47 | } 48 | 49 | &-iframe { 50 | display: block; 51 | width: 100%; 52 | height: 100%; 53 | border: none; 54 | } 55 | &[loading] &-iframe { 56 | position: absolute; 57 | visibility: hidden; 58 | max-height: 100%; 59 | inset: 0; 60 | } 61 | 62 | &.centered-content &-inner { 63 | margin: auto; 64 | } 65 | 66 | &-v-scroll, 67 | &-h-scroll { 68 | position: relative; 69 | overflow: hidden; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | 74 | &-v-scroll { 75 | grid-area: v-scroll; 76 | width: 16px; 77 | transition: width 0.25s linear; 78 | &[inactive] { 79 | display: block; 80 | visibility: hidden; 81 | width: 0; 82 | } 83 | } 84 | 85 | &-h-scroll { 86 | grid-area: h-scroll; 87 | height: 16px; 88 | transition: height 0.25s linear; 89 | &[inactive] { 90 | display: block; 91 | visibility: hidden; 92 | height: 0; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/core/processors/normalization.ts: -------------------------------------------------------------------------------- 1 | import {UIPPreprocessorService} from './preprocessor'; 2 | 3 | /** 4 | * Normalization processors store for JS content. 5 | * Normalization transformation are used to normalize content before displaying or processing. 6 | */ 7 | export const UIPJSNormalizationPreprocessors = new UIPPreprocessorService(); 8 | /** 9 | * Normalization processors store for HTML content. 10 | * Normalization transformation are used to normalize content before displaying or processing. 11 | */ 12 | export const UIPHTMLNormalizationPreprocessors = new UIPPreprocessorService(); 13 | /** 14 | * Normalization processors store for Notes content (could be HTML). 15 | * Normalization transformation are used to normalize content before displaying or processing. 16 | */ 17 | export const UIPNoteNormalizationPreprocessors = new UIPPreprocessorService(); 18 | 19 | 20 | /** Removes extra indents to beautify content alignment */ 21 | export function removeIndent(input: string): string { 22 | // Get all indents from text 23 | const indents = input.match(/^[^\S\n\r]*(?=\S)/gm); 24 | // No processing if no indent on the first line or input is empty 25 | if (!indents || !indents[0].length) return input; 26 | // Sort indents by length 27 | indents.sort((a, b) => a.length - b.length); 28 | // No processing if minimal indent is 0 29 | if (!indents[0].length) return input; 30 | // Remove indents from text 31 | return input.replace(RegExp('^' + indents[0], 'gm'), ''); 32 | } 33 | UIPJSNormalizationPreprocessors.add('remove-leading-indent', removeIndent); 34 | UIPHTMLNormalizationPreprocessors.add('remove-leading-indent', removeIndent); 35 | UIPNoteNormalizationPreprocessors.add('remove-leading-indent', removeIndent); 36 | 37 | /** Trim content */ 38 | UIPJSNormalizationPreprocessors.add('trim', (content: string) => content.trim()); 39 | UIPHTMLNormalizationPreprocessors.add('trim', (content: string) => content.trim()); 40 | UIPNoteNormalizationPreprocessors.add('trim', (content: string) => content.trim()); 41 | -------------------------------------------------------------------------------- /src/plugins/direction/dir-toggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {UIPPluginButton} from '../../core/button/plugin-button'; 5 | import type {UIPPreview} from '../../core/preview/preview'; 6 | 7 | import './dir-toggle.shape'; 8 | 9 | /** Text direction switcher button-plugin for UI Playground widget */ 10 | export class UIPDirSwitcher extends UIPPluginButton { 11 | public static override is = 'uip-dir-toggle'; 12 | public static override defaultTitle = 'Switch direction'; 13 | 14 | @memoize() 15 | get $preview(): UIPPreview { 16 | return this.$root!.querySelector('uip-preview')!; 17 | } 18 | 19 | @memoize() 20 | get $content(): HTMLElement { 21 | const type = this.constructor as typeof UIPDirSwitcher; 22 | return ( 23 |
24 | L 25 | T 26 | R 27 |
28 | ) as HTMLElement; 29 | } 30 | @memoize() 31 | get $label(): HTMLElement { 32 | const type = this.constructor as typeof UIPDirSwitcher; 33 | return (
{this.label || this.title}
) as HTMLElement; 34 | } 35 | 36 | protected override connectedCallback(): void { 37 | if (!this.$root || !this.$preview) return; 38 | this.$$attr('uip-settings-content', true); 39 | this.$$fire('uip:settings:invalidate'); 40 | super.connectedCallback(); 41 | this.appendChild(this.$label); 42 | this.appendChild(this.$content); 43 | this.onDirChange(); 44 | } 45 | 46 | protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { 47 | super.attributeChangedCallback(attrName, oldVal, newVal); 48 | if (attrName === 'label') this.$label.textContent = this.label || this.title; 49 | } 50 | 51 | protected override onAction(): void { 52 | if (!this.$preview) return; 53 | this.$preview.dir = this.$content.dir === 'rtl' ? 'ltr' : 'rtl'; 54 | } 55 | 56 | @listen({ 57 | event: 'uip:dirchange', 58 | target: ($this: UIPDirSwitcher) => $this.$preview, 59 | }) 60 | protected onDirChange(): void { 61 | this.$content.dir = this.$preview.dir; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /site/views/examples/example/playground.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example with a playground that contains playground inside 3 | --- 4 | 5 | 6 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/plugins/settings/settings.less: -------------------------------------------------------------------------------- 1 | @import '../../../site/src/variables'; 2 | 3 | uip-settings { 4 | display: none; 5 | } 6 | .uip-settings { 7 | grid-area: settings; 8 | 9 | accent-color: var(--uip-plugin-fg, #000); 10 | 11 | &-inner { 12 | flex: 1 1 auto; 13 | display: flex; 14 | position: relative; 15 | overflow: hidden; 16 | padding: 20px 10px; 17 | } 18 | 19 | &-container { 20 | display: flex; 21 | flex: 1 1 auto; 22 | flex-direction: column; 23 | order: 0; 24 | gap: 20px; 25 | width: 100%; 26 | } 27 | 28 | &-scrollbar { 29 | flex: 0 0 auto; 30 | order: 1; 31 | } 32 | 33 | &.vertical { 34 | .uip-settings-container { 35 | min-width: 200px; 36 | } 37 | .uip-settings-scrollbar { 38 | position: relative; 39 | padding-right: 0; 40 | 41 | &[inactive] { 42 | display: none; 43 | } 44 | } 45 | } 46 | 47 | &:not(.vertical) { 48 | .uip-settings-scrollbar { 49 | order: -1; 50 | opacity: 0; 51 | transition: opacity 0.3s ease-in-out; 52 | } 53 | &:not([collapsed]) .uip-settings-scrollbar:not([inactive]) { 54 | opacity: 1; 55 | transition-delay: 0.5s; 56 | } 57 | } 58 | 59 | &-toolbar-option { 60 | width: 1.25rem; 61 | height: 1.25rem; 62 | } 63 | 64 | &.vertical .uip-plugin-header-icon { 65 | &::after { 66 | content: ''; 67 | position: relative; 68 | top: 4px; 69 | width: 15px; 70 | height: 15px; 71 | background: currentColor; 72 | clip-path: polygon(20% 50%, 70% 0%, 80% 5%, 35% 50%, 80% 95%, 70% 100%); 73 | } 74 | 75 | .settings-icon { 76 | display: none; 77 | } 78 | } 79 | 80 | // Rotate on collapsed state change 81 | &[collapsed] .uip-plugin-header-icon { 82 | animation: rotate-back 0.5s linear both; 83 | } 84 | &:not([collapsed]) .uip-plugin-header-icon { 85 | animation: rotate 0.5s linear both; 86 | } 87 | 88 | &[inactive]&[hideable] { 89 | display: none; 90 | } 91 | } 92 | 93 | @keyframes rotate { 94 | 0% { 95 | transform: rotate(0deg); 96 | } 97 | 100% { 98 | transform: rotate(180deg); 99 | } 100 | } 101 | @keyframes rotate-back { 102 | 0% { 103 | transform: rotate(180deg); 104 | } 105 | 100% { 106 | transform: rotate(0deg); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /site/views/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: basic 3 | isLandingPage: true 4 | --- 5 |
6 |
7 |

UIPlayground - is a UI library that allows you to present and demonstrate any other library and your custom components.

8 |
9 |

With the help of UIP components we allow you to 'play' with a component.

10 |

You can choose from the variety of component's templates, play with the component's settings or even change its 11 | markup!

12 |
13 |
14 | 15 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Install UIPlayground [npm dependency](https://www.npmjs.com/package/@exadel/ui-playground): 4 | ```bash 5 | npm i @exadel/ui-playground --save 6 | ``` 7 | 8 | # Project structure 9 | 10 | UIP components are organized in the following way: 11 | 12 | Core Elements 13 | - [UIP Root](src/core/README.md#uip-root) 14 | - [UIP Preview](src/core/README.md) 15 | 16 | Plugins 17 | - [UIP Editor](src/plugins/editor/README.md) 18 | - [UIP Settings and Setting](src/plugins/settings/README.md) 19 | - [UIP Text Setting](src/plugins/settings/text-setting/README.md) 20 | - [UIP Bool Setting](src/plugins/settings/bool-setting/README.md) 21 | - [UIP Select Setting](src/plugins/settings/select-setting/README.md) 22 | - [UIP Snippets](src/plugins/snippets/README.md) 23 | - [UIP Snippets Title](src/plugins/snippets-title/README.md) 24 | - [UIP Snippets List](src/plugins/snippets-list/README.md) 25 | - [UIP Theme Toggle](src/plugins/theme/README.md) 26 | - [UIP Note](src/plugins/note/README.md) 27 | - [UIP Copy](src/plugins/copy/README.md) 28 | - [UIP Text Direction Toggle](src/plugins/direction/README.md) 29 | --- 30 | 31 | UIPlayground must have at least **Сore** components. **Plugins** are 32 | optional, you can add them on your own free will. 33 | 34 | To implement custom UIPlayground components, see [UIPPlugin](src/core/README.md#uip-plugin). 35 | 36 | # Modules/components imports 37 | To register all components, you can use the next callback: 38 | 39 | ```typescript 40 | import {init} from '@exadel/ui-playground/esm/registration'; 41 | init(); 42 | ``` 43 | 44 | There is also an ability to register only Core/Plugins/Settings parts. To do this, call one of the functions below: 45 | 46 | ```typescript 47 | import {registerCore, registerPlugins, registerSettings} from '@exadel/ui-playground/esm/registration'; 48 | registerCore(); 49 | registerPlugins(); 50 | registerSettings(); 51 | ``` 52 | 53 | The callbacks above register UIP components by themselves. But if you want to have a custom registration logic, 54 | there is a way to register components manually: 55 | 56 | ```typescript 57 | import {UIPRoot} from '@exadel/ui-playground/esm/registration'; 58 | UIPRoot.register(); 59 | ``` 60 | 61 | Every module has two versions of styles: *css* and *less*. If you want 62 | to import styles for all UIP component, you can import either 63 | *registration.less* or *registration.css* file: 64 | 65 | ```less 66 | @import '@exadel/ui-playground/esm/registration.css'; 67 | ``` 68 | 69 | # Browser support 70 | 71 | UIPlayground supports all modern browsers. 72 | -------------------------------------------------------------------------------- /site/views/examples/example/text.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example with dummy text (pre-processors) 3 | --- 4 | 5 | 6 | 7 | 8 | 23 | 30 | 44 | 51 | 72 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/plugins/settings/slider-setting/slider-setting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {attr, memoize, listen} from '@exadel/esl/modules/esl-utils/decorators'; 4 | 5 | import {UIPSetting} from '../base-setting/base-setting'; 6 | import type {UIPStateModel} from '../../../core/base/model'; 7 | 8 | export class UIPSliderSetting extends UIPSetting { 9 | public static is = 'uip-slider-setting'; 10 | 11 | /** Setting's visible name. */ 12 | @attr({defaultValue: ''}) public label: string; 13 | /** Minimum range value. */ 14 | @attr({defaultValue: '0'}) public min: string; 15 | /** Maximum range value. */ 16 | @attr({defaultValue: '0'}) public max: string; 17 | /** Step for range. */ 18 | @attr({defaultValue: '0'}) public step: string; 19 | 20 | protected override connectedCallback(): void { 21 | super.connectedCallback(); 22 | 23 | const $inner = 24 | <> 25 | 29 | {this.$fieldValue} 30 | ; 31 | 32 | this.append($inner); 33 | } 34 | 35 | protected override disconnectedCallback(): void { 36 | this.innerHTML = ''; 37 | super.disconnectedCallback(); 38 | } 39 | 40 | /** Range input to change setting's value */ 41 | @memoize() 42 | protected get $field(): HTMLInputElement { 43 | return as HTMLInputElement; 44 | } 45 | 46 | /** Container for current value */ 47 | @memoize() 48 | protected get $fieldValue(): HTMLElement { 49 | return
as HTMLElement; 50 | } 51 | 52 | /** Handles `input` event to display its current value */ 53 | @listen('input') 54 | protected updateSliderValue(): void { 55 | this.$fieldValue.textContent = `Value: ${this.$field.value}`; 56 | } 57 | 58 | updateFrom(model: UIPStateModel): void { 59 | super.updateFrom(model); 60 | const values = model.getAttribute(this.target, this.attribute); 61 | if (!values.length) { 62 | this.setInconsistency(this.NO_TARGET_MSG); 63 | } else if (!values[0]) { 64 | this.setInconsistency(this.NOT_VALUE_SPECIFIED_MSG); 65 | } else { 66 | this.setValue(values[0]); 67 | } 68 | } 69 | 70 | protected getDisplayedValue(): string { 71 | return this.$field.value; 72 | } 73 | 74 | protected setValue(value: string | null): void { 75 | if (value) { 76 | this.$field.value = value; 77 | this.updateSliderValue(); 78 | } 79 | } 80 | 81 | protected setInconsistency(msg = this.INCONSISTENT_VALUE_MSG): void { 82 | this.$fieldValue.textContent = msg; 83 | } 84 | 85 | public setDisabled(force: boolean): void { 86 | this.$fieldValue.classList.toggle('disabled', force); 87 | this.$field.toggleAttribute('disabled', force); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /site/src/page/header.less: -------------------------------------------------------------------------------- 1 | @import '~@exadel/esl/modules/esl-trigger/core'; 2 | 3 | .header { 4 | height: @header-height-desktop; 5 | font-size: 20px; 6 | color: @light-color; 7 | background-color: @landing-dark-bg; 8 | 9 | @media @mobile { 10 | height: @header-height-mobile; 11 | } 12 | 13 | &-container { 14 | display: flex; 15 | align-items: center; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | .uip-title, 21 | .header-nav-link { 22 | color: @light-color; 23 | } 24 | 25 | .uip-title { 26 | display: flex; 27 | align-items: center; 28 | padding: 10px 0; 29 | font-weight: bold; 30 | 31 | &-text { 32 | margin-left: 10px; 33 | } 34 | 35 | @media @mobile { 36 | margin-left: 0; 37 | } 38 | 39 | .uip-logo { 40 | width: 64px; 41 | height: 64px; 42 | 43 | @media @mobile { 44 | width: 48px; 45 | height: 48px; 46 | } 47 | } 48 | } 49 | 50 | .header-nav { 51 | display: flex; 52 | margin-left: auto; 53 | align-items: center; 54 | 55 | @media @mobile { 56 | margin-right: 0; 57 | } 58 | 59 | &-link { 60 | margin-left: 40px; 61 | 62 | @media @tablet { 63 | margin-left: 20px; 64 | } 65 | 66 | @media @mobile { 67 | display: none; 68 | } 69 | 70 | &.icon-link { 71 | display: flex; 72 | 73 | @media @mobile { 74 | display: none; 75 | } 76 | } 77 | } 78 | 79 | .get-started { 80 | padding: 6px 16px; 81 | border: 1px solid white; 82 | border-radius: 10px; 83 | font-size: 18px; 84 | background-color: @light-color; 85 | color: @dark-text; 86 | } 87 | 88 | .header-nav-menu { 89 | display: none; 90 | 91 | @media @mobile { 92 | display: block; 93 | } 94 | 95 | position: relative; 96 | width: 30px; 97 | height: 22px; 98 | margin: 0 2px; 99 | 100 | border-top: 2px solid white; 101 | border-bottom: 2px solid white; 102 | 103 | transition: border 0.5s ease-in-out; 104 | 105 | &::before, 106 | &::after { 107 | content: ''; 108 | display: block; 109 | width: 100%; 110 | height: 2px; 111 | 112 | position: absolute; 113 | top: 50%; 114 | left: 50%; 115 | 116 | background: white; 117 | 118 | transform: translate(-50%, -50%); 119 | transition: transform 0.5s ease-in-out; 120 | } 121 | 122 | &[active] { 123 | border-color: transparent; 124 | 125 | &::before { 126 | transform: translate(-50%, -50%) rotate(45deg); 127 | } 128 | &::after { 129 | transform: translate(-50%, -50%) rotate(-45deg); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/plugins/settings/README.md: -------------------------------------------------------------------------------- 1 | # UIP Settings 2 | 3 | **UIPSettings** - custom element which stores settings (**UIPSetting**). 4 | Extends [UIPPlugin](src/core/README.md#uip-plugin). 5 | 6 | **UIPSettings** is used as a container for **UIPSetting** elements. It serves as a link between 7 | our standard UIP flow for change detection and settings updates. 8 | **UIPSettings** provides active internal settings items and updates them when the state changes. 9 | 10 | **UIPSettings** has also its own toolbar where the theme or text direction for UIPlayground can be specified using the *theme-toggle* and *dir-toggle* attributes. 11 | 12 | # UIP Setting 13 | 14 | **UIPSetting** - custom element for manipulating element attributes. Custom settings should extend 15 | *UIPSetting* class. 16 | 17 | **UIPSetting** processes markup to update own value via *updateFrom()* and updates it with *applyTo()*. 18 | **UIPSetting** dispatches *uip:change* event to let **UIPSettings** know about setting changes. 19 | 20 | These things have default implementation. 21 | There are also *isValid()* and *setInconsistency()* methods to handle incorrect setting states. 22 | *isValid()* can be used to add custom validation and *setInconsistency()* is used to let user know about inconsistent state (when there are multiple setting values, no target, etc.). 23 | 24 | Methods needed to be implemented: 25 | - **getDisplayedValue()** to get value from custom setting. 26 | - **setValue()** to set setting's value. 27 | 28 | The following attributes used: 29 | - **label** - setting's displayed name. 30 | - **target** - sets target to which the setting is attached. If you want to set the same target for all settings, use *target* attribute on *UIPSettings*. 31 | 32 | Examples of existing custom settings: 33 | - [UIPTextSetting](src/plugins/text-setting/README.md) 34 | - [UIPBoolSetting](src/plugins/bool-setting/README.md) 35 | - [UIPSelectSetting](src/plugins/select-setting/README.md) 36 | - [UIPSliderSetting](src/plugins/slider-setting/README.md) 37 | 38 | ## Example 39 | 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /src/core/base/snippet.ts: -------------------------------------------------------------------------------- 1 | import {memoize} from '@exadel/esl/modules/esl-utils/decorators'; 2 | 3 | /** Type for both 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 |
36 |

List of cards

37 |

38 | Card list is a component for displaying multiple card's info. 39 | Can manually wrap list items on new lines. 40 |

41 | 42 | 43 |
44 | 45 | 46 |
47 | 54 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /src/plugins/snippets/snippets.less: -------------------------------------------------------------------------------- 1 | .uip-snippets { 2 | position: relative; 3 | height: 100%; 4 | width: 100%; 5 | 6 | .uip-snippets-trigger { 7 | display: none; 8 | } 9 | 10 | .uip-snippets-list-inner { 11 | display: flex; 12 | flex-wrap: wrap; 13 | gap: 0.5rem; 14 | 15 | width: 100%; 16 | list-style-type: none; 17 | 18 | margin: 0; 19 | padding: 0 10px; 20 | } 21 | 22 | .uip-snippets-item { 23 | display: block; 24 | position: relative; 25 | padding: 6px 10px; 26 | 27 | font-weight: 500; 28 | 29 | &::after { 30 | content: ''; 31 | position: absolute; 32 | width: 100%; 33 | height: 2px; 34 | left: 0; 35 | bottom: 0; 36 | 37 | opacity: 0; 38 | background: var(--uip-plugin-accent, transparent); 39 | 40 | transition: opacity 0.3s ease-in-out; 41 | } 42 | 43 | &:hover { 44 | cursor: pointer; 45 | opacity: 0.7; 46 | } 47 | } 48 | 49 | .uip-snippets-item-active .uip-snippets-item::after { 50 | opacity: 1; 51 | } 52 | } 53 | 54 | .uip-snippets[view='dropdown'] { 55 | .uip-snippets-item-active .uip-snippets-item::after { 56 | display: none; 57 | } 58 | 59 | .uip-snippets-dropdown { 60 | position: absolute; 61 | display: flex; 62 | top: 100%; 63 | left: 0; 64 | z-index: 10; 65 | width: auto; 66 | min-width: 33.33%; 67 | max-width: 100%; 68 | font-size: 1rem; 69 | 70 | color: var(--uip-plugin-fg, #000); 71 | background-color: var(--uip-plugin-bg, #fff); 72 | 73 | visibility: hidden; 74 | max-height: 0; 75 | 76 | transition: 77 | max-height 0.3s ease-in-out, 78 | visibility 0.3s, 79 | box-shadow 0.3s ease-in-out; 80 | 81 | &.open { 82 | max-height: 250px; 83 | visibility: visible; 84 | box-shadow: 0 4px 5px -1px var(--uip-plugin-fg, #000); 85 | transition: 86 | max-height 0.3s ease-in-out, 87 | visibility 0s; 88 | } 89 | } 90 | 91 | .uip-snippets-list { 92 | position: relative; 93 | display: flex; 94 | width: 100%; 95 | max-height: 100%; 96 | overflow: hidden; 97 | } 98 | 99 | .uip-snippets-list-inner { 100 | flex-wrap: nowrap; 101 | flex-direction: column; 102 | 103 | gap: 0; 104 | padding: 0; 105 | } 106 | 107 | .uip-snippets-scroll { 108 | opacity: 0; 109 | transition: opacity 0.3s linear; 110 | } 111 | 112 | .uip-snippets-dropdown.open { 113 | .uip-snippets-scroll { 114 | opacity: 1; 115 | transition-delay: 0.4s; 116 | } 117 | } 118 | 119 | .uip-snippets-item { 120 | font-weight: 400; 121 | padding: 6px 24px 8px; 122 | text-align: start; 123 | } 124 | 125 | .uip-snippets-item-wrapper { 126 | border-top: 1px solid var(--uip-plugin-divider, #ccc); 127 | } 128 | 129 | .uip-snippets-trigger { 130 | cursor: pointer; 131 | display: flex; 132 | align-items: center; 133 | height: 100%; 134 | 135 | svg { 136 | height: 8px; 137 | margin: 0 5px; 138 | pointer-events: none; 139 | 140 | stroke: currentColor; 141 | 142 | transform-origin: center; 143 | transition: transform 0.3s ease-in-out; 144 | } 145 | 146 | &:hover { 147 | opacity: 0.8; 148 | } 149 | } 150 | 151 | .uip-snippets-trigger.open { 152 | svg { 153 | transform: rotateX(180deg); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exadel/ui-playground", 3 | "version": "2.1.0-beta.10", 4 | "description": "UIPlayground is a solution for presenting your custom components.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "playground", 8 | "demo", 9 | "presentation", 10 | "editor", 11 | "custom elements" 12 | ], 13 | "publishConfig": { 14 | "access": "public", 15 | "scope": "@exadel" 16 | }, 17 | "types": "esm/registration.d.ts", 18 | "module": "esm/registration.js", 19 | "style": "esm/registration.css", 20 | "files": [ 21 | "tsconfig.json", 22 | "README.md", 23 | "CLA.md", 24 | "CHANGELOG.md", 25 | "esm/**/*.{js,ts,css,less}" 26 | ], 27 | "sideEffects": [ 28 | "src/**/*.less" 29 | ], 30 | "scripts": { 31 | "start": "cd site && npm run start", 32 | "clean": "rimraf esm", 33 | "build": "npm run clean && npm run build:less && npm run build:ts", 34 | "build:ts": "tsc --project tsconfig.json", 35 | "build:less": "npm run build:less:cpy && npm run build:less:css", 36 | "build:less:cpy": "copyfiles -u 1 \"./src/**/*.less\" \"esm\"", 37 | "build:less:css": "findx \"esm/{*,*/*}.less\" \"lessc {{path}} {{dir}}/{{name}}.css\"", 38 | "watch": "concurrently \"npm run watch:less\" \"npm run watch:ts\"", 39 | "watch:ts": "tsc --project tsconfig.json --watch", 40 | "watch:less": "chokidar --initial \"src/**/*.less\" -c \"npm run build:less\"", 41 | "lint": "concurrently \"npm run lint:js\" \"npm run lint:css\"", 42 | "lint:js": "eslint src/**/*.ts", 43 | "lint:css": "stylelint src/**/*.less", 44 | "prepare": "husky && npm run build", 45 | "version": "cross-env-shell \"npm version $npm_package_version --no-git-tag-version --ws\" && git add **/package*.json" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/exadel-inc/ui-playground.git" 50 | }, 51 | "contributors": [ 52 | "Yuliya Adamskaya ", 53 | "Harshunova Natallia ", 54 | "Palanevich Aliaksandr ", 55 | "Valchetskaya Palina ", 56 | "Bazukevich Aliaksandr " 57 | ], 58 | "bugs": { 59 | "url": "https://github.com/exadel-inc/ui-playground/issues" 60 | }, 61 | "homepage": "https://github.com/exadel-inc/ui-playground#readme", 62 | "engines": { 63 | "node": ">=18.17.0" 64 | }, 65 | "workspaces": [ 66 | "site" 67 | ], 68 | "dependencies": { 69 | "@exadel/esl": "^5.3.2", 70 | "codejar": "^4.2.0", 71 | "jsx-dom": "6.4.23", 72 | "prismjs": "1.30.0" 73 | }, 74 | "devDependencies": { 75 | "@commitlint/cli": "^19.7.1", 76 | "@commitlint/config-conventional": "^19.7.1", 77 | "@exadel/eslint-config-esl": "^5.3.2", 78 | "@exadel/eslint-plugin-esl": "^5.3.2", 79 | "@exadel/stylelint-config-esl": "^5.3.2", 80 | "@semantic-release/changelog": "^6.0.3", 81 | "@semantic-release/commit-analyzer": "^13.0.1", 82 | "@semantic-release/git": "^10.0.1", 83 | "@semantic-release/github": "^11.0.1", 84 | "@semantic-release/npm": "^12.0.1", 85 | "@semantic-release/release-notes-generator": "^14.0.3", 86 | "@types/prismjs": "^1.26.5", 87 | "chokidar-cli": "^3.0.0", 88 | "concurrently": "^9.1.2", 89 | "copyfiles": "^2.4.1", 90 | "cross-env": "^7.0.3", 91 | "eslint": "^9.25.1", 92 | "findx-cli": "^0.2.2", 93 | "husky": "^9.1.7", 94 | "less": "^4.3.0", 95 | "lint-staged": "^15.5.1", 96 | "postcss-less": "^6.0.0", 97 | "rimraf": "^6.0.1", 98 | "semantic-release": "^24.2.3", 99 | "stylelint": "^16.18.0", 100 | "typescript": "5.8.3" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the Core Team (Alexey Stsefanovich , Yuliya Adamskaya ). 63 | All complaints will be reviewed and investigated and will 64 | result in a response that is deemed necessary and appropriate to the circumstances. 65 | The project team is obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 75 | version 2.0, available at 76 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 77 | 78 | [homepage]: https://www.contributor-covenant.org 79 | 80 | For answers to common questions about this code of conduct, see the FAQ at 81 | https://www.contributor-covenant.org/faq. Translations are available at 82 | https://www.contributor-covenant.org/translations. 83 | -------------------------------------------------------------------------------- /src/plugins/snippets/snippets.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {attr, listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {ESLMediaQuery} from '@exadel/esl/modules/esl-media-query/core'; 5 | import {UIPPlugin} from '../../core/base/plugin'; 6 | import {UIPSnippetsIcon} from '../snippets-list/snippets.icon'; 7 | 8 | import type {ESLTrigger} from '@exadel/esl/modules/esl-trigger/core'; 9 | import type {ESLToggleable} from '@exadel/esl/modules/esl-toggleable/core'; 10 | import type {UIPSnippetsTitle} from '../snippets-title/snippets-title'; 11 | 12 | /** 13 | * Snippets {@link UIPPlugin} custom element definition 14 | * Container for {@link UIPSnippetsList} element 15 | */ 16 | export class UIPSnippets extends UIPPlugin { 17 | static override is = 'uip-snippets'; 18 | static override observedAttributes = ['dropdown-view', ...UIPPlugin.observedAttributes]; 19 | 20 | @attr({defaultValue: 'not all'}) public dropdownView: string; 21 | 22 | /** @returns true if dropdown mode should be active */ 23 | public get isDropdown(): boolean { 24 | return ESLMediaQuery.for(this.dropdownView).matches; 25 | } 26 | 27 | 28 | /** Builds inner {@link UIPSnippetsTitle} */ 29 | @memoize() 30 | protected get $title(): UIPSnippetsTitle { 31 | return as UIPSnippetsTitle; 32 | } 33 | 34 | /** Builds trigger area for dropdown mode */ 35 | @memoize() 36 | protected get $trigger(): ESLTrigger { 37 | return ( 38 | 39 | 40 | {this.$title} 41 | 42 | ) as ESLTrigger; 43 | } 44 | 45 | /** Builds dropdown/main area */ 46 | @memoize() 47 | protected get $toggleable(): ESLToggleable { 48 | return ( 49 | 50 | 51 | 52 | 53 | ) as ESLToggleable; 54 | } 55 | 56 | protected override connectedCallback(): void { 57 | super.connectedCallback(); 58 | if (this.$root?.ready) this._onRootReady(); 59 | } 60 | 61 | protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { 62 | super.attributeChangedCallback(attrName, oldVal, newVal); 63 | if (attrName === 'dropdown-view') { 64 | this.$$off(this._onBreakpointChange); 65 | this.$$on(this._onBreakpointChange); 66 | this._onBreakpointChange(); 67 | } 68 | } 69 | 70 | /** Renders {@link UIPSnippetsList} element */ 71 | protected _renderSnippetsList(): void { 72 | switch (this.model?.snippets.length) { 73 | case 0: return; 74 | case 1: 75 | this.prepend(this.$title); 76 | break; 77 | default: 78 | this.prepend(this.$trigger, this.$toggleable); 79 | break; 80 | } 81 | } 82 | 83 | @listen({event: 'uip:root:ready', target: ($this: UIPPlugin) => $this.$root}) 84 | protected _onRootReady(): void { 85 | this._renderSnippetsList(); 86 | this._onBreakpointChange(); 87 | } 88 | 89 | @listen({ 90 | event: 'change', 91 | target: (snippets: UIPSnippets) => ESLMediaQuery.for(snippets.dropdownView) 92 | }) 93 | protected _onBreakpointChange(): void { 94 | const isDropdown = ESLMediaQuery.for(this.dropdownView).matches; 95 | // Toggleable is open in tabs mode 96 | this.$toggleable.toggle(!isDropdown); 97 | this.$$attr('view', isDropdown ? 'dropdown' : 'tabs'); 98 | } 99 | 100 | @listen('esl:before:hide') 101 | protected _onBeforeHide(e: Event): void { 102 | // Prevent hide toggleable in inactive state (tabs) 103 | if (e.target === this.$toggleable && !this.isDropdown) e.preventDefault(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/plugins/snippets-list/snippets-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {attr, decorate, listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 4 | import {debounce} from '@exadel/esl/modules/esl-utils/async/debounce'; 5 | 6 | import {UIPPlugin} from '../../core/base/plugin'; 7 | import {UIPSnippets} from '../snippets/snippets'; 8 | 9 | 10 | import './snippets-list.shape'; 11 | 12 | import type {UIPSnippetItem} from '../../core/base/snippet'; 13 | import type {DelegatedEvent} from '@exadel/esl/modules/esl-event-listener/core'; 14 | import type {UIPRoot} from '../../core/base/root'; 15 | 16 | 17 | /** 18 | * Snippets List {@link UIPPlugin} custom element definition 19 | * Container class for snippets (component's templates) 20 | */ 21 | export class UIPSnippetsList extends UIPPlugin { 22 | public static override is = 'uip-snippets-list'; 23 | 24 | /** CSS Class for snippets list item */ 25 | @attr({defaultValue: 'uip-snippets-item-wrapper'}) public itemCls: string; 26 | /** CSS Class for snippets button */ 27 | @attr({defaultValue: 'uip-snippets-item'}) public itemBtnCls: string; 28 | /** CSS Class added to active snippet */ 29 | @attr({defaultValue: 'uip-snippets-item-active'}) public activeCls: string; 30 | /** CSS Class added to isolated snippet */ 31 | @attr({defaultValue: 'uip-snippets-item-isolated'}) public isolatedCls: string; 32 | 33 | @memoize() 34 | public override get $root(): UIPRoot | null { 35 | const parent: UIPSnippets = this.closest(`${UIPSnippets.is}`)!; 36 | if (parent) return parent.$root; 37 | return super.$root; 38 | } 39 | 40 | /** {@link UIPPlugin} section wrapper */ 41 | @memoize() 42 | protected get $inner(): HTMLElement { 43 | const pluginType = this.constructor as typeof UIPPlugin; 44 | return
    {this.$items}
as HTMLElement; 45 | } 46 | 47 | /** Snippets list from dropdown element*/ 48 | @memoize() 49 | public get $items(): HTMLElement[] { 50 | if (!this.model) return []; 51 | return this.model.snippets.map(this.buildListItem.bind(this)).filter((item: HTMLElement): item is HTMLElement => !!item); 52 | } 53 | 54 | protected override connectedCallback(): void { 55 | super.connectedCallback(); 56 | this.appendChild(this.$inner); 57 | } 58 | 59 | protected override disconnectedCallback(): void { 60 | super.disconnectedCallback(); 61 | this.innerHTML = ''; 62 | memoize.clear(this, ['$items', '$inner']); 63 | } 64 | 65 | /** Initializes snippets layout */ 66 | @decorate(debounce, 0) 67 | protected rerender(): void { 68 | this.$inner.innerHTML = ''; 69 | memoize.clear(this, '$items'); 70 | this.$inner.append(...this.$items); 71 | } 72 | 73 | /** Builds snippets list item */ 74 | protected buildListItem(snippet: UIPSnippetItem, index: number): HTMLElement | undefined { 75 | if (!snippet.label) return; 76 | 77 | const classes = [this.itemCls]; 78 | if (snippet.active) classes.push(this.activeCls); 79 | if (snippet.isolated) classes.push(this.isolatedCls); 80 | 81 | return
  • 82 | 83 |
  • as HTMLElement; 84 | } 85 | 86 | /** Handles `click` event to manage active snippet */ 87 | @listen({event: 'click', selector: '[uip-snippet-index]'}) 88 | protected _onItemClick(e: DelegatedEvent): void { 89 | const {snippets} = this.model!; 90 | const index = e.$delegate?.getAttribute('uip-snippet-index') || ''; 91 | if (!index || !snippets) return; 92 | this.model!.applySnippet(snippets[+index], this); 93 | e.preventDefault(); 94 | } 95 | 96 | /** Handles `uip:snippet:change` event to rerender snippets list */ 97 | @listen({event: 'uip:snippet:change', target: ($this: UIPSnippetsList) => $this.$root}) 98 | protected _onRootStateChange(): void { 99 | this.rerender(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/core/base/root.ts: -------------------------------------------------------------------------------- 1 | import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core'; 2 | import { 3 | memoize, 4 | boolAttr, 5 | listen, 6 | prop, 7 | attr 8 | } from '@exadel/esl/modules/esl-utils/decorators'; 9 | 10 | import {UIPStateModel} from './model'; 11 | import {UIPChangeEvent} from './model.change'; 12 | import {UIPStateStorage} from './state.storage'; 13 | 14 | import type {UIPSnippetTemplate} from './snippet'; 15 | import type {UIPChangeInfo} from './model.change'; 16 | 17 | /** 18 | * UI Playground root custom element definition 19 | * Container element for {@link UIPPlugin} components 20 | * Defines the bounds of UI Playground instance 21 | * Shares the {@link UIPStateModel} instance between {@link UIPPlugin}-s 22 | */ 23 | export class UIPRoot extends ESLBaseElement { 24 | public static is = 'uip-root'; 25 | public static observedAttributes = ['dark-theme']; 26 | 27 | /** Event dispatching on the {@link UIPRoot} ready state */ 28 | @prop('uip:root:ready') public READY_EVENT: string; 29 | /** Event dispatching on the {@link UIPStateModel} state change */ 30 | @prop('uip:change') public CHANGE_EVENT: string; 31 | /** Event dispatching on the {@link UIPStateModel} current snippet change */ 32 | @prop('uip:snippet:change') public SNIPPET_CHANGE_EVENT: string; 33 | /** Event dispatching on the {@link UIPRoot} theme attribute change */ 34 | @prop('uip:theme:change') public THEME_CHANGE_EVENT: string; 35 | 36 | /** CSS query for snippets */ 37 | public static SNIPPET_SEL = '[uip-snippet]'; 38 | 39 | /** Indicates that the UIP components' theme is dark */ 40 | @boolAttr() public darkTheme: boolean; 41 | /** Key to store UIP state in the local storage */ 42 | @attr({defaultValue: ''}) public storeKey: string; 43 | /** State storage based on `storeKey` */ 44 | public storage: UIPStateStorage | undefined; 45 | 46 | /** Indicates ready state of the uip-root */ 47 | @boolAttr({readonly: true}) public ready: boolean; 48 | 49 | /** {@link UIPStateModel} instance to store UI Playground state */ 50 | @memoize() 51 | public get model(): UIPStateModel { 52 | return new UIPStateModel(); 53 | } 54 | 55 | /** Collects snippets template-holders */ 56 | public get $snippets(): UIPSnippetTemplate[] { 57 | return Array.from(this.querySelectorAll(UIPRoot.SNIPPET_SEL)); 58 | } 59 | 60 | protected delayedScrollIntoView(): void { 61 | setTimeout(() => { 62 | this.scrollIntoView({behavior: 'smooth', block: 'start'}); 63 | }, 100); 64 | } 65 | 66 | protected override connectedCallback(): void { 67 | super.connectedCallback(); 68 | if (this.storeKey) this.storage = new UIPStateStorage(this.storeKey, this.model); 69 | 70 | this.model.snippets = this.$snippets; 71 | this.model.applyCurrentSnippet(this); 72 | this.$$attr('ready', true); 73 | this.$$fire(this.READY_EVENT, {bubbles: false}); 74 | 75 | if (this.model.anchorSnippet) { 76 | this.delayedScrollIntoView(); 77 | } 78 | } 79 | 80 | protected override disconnectedCallback(): void { 81 | super.disconnectedCallback(); 82 | this.model.snippets = []; 83 | } 84 | 85 | protected attributeChangedCallback( 86 | name: string, 87 | oldValue: string | null, 88 | newValue: string | null 89 | ): void { 90 | super.attributeChangedCallback(name, oldValue, newValue); 91 | if (name === 'dark-theme') { 92 | this.$$fire(this.THEME_CHANGE_EVENT, { 93 | detail: this.darkTheme, 94 | bubbles: false 95 | }); 96 | } 97 | } 98 | 99 | @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model}) 100 | protected onModelChange({detail}: CustomEvent): void { 101 | this.dispatchEvent(new UIPChangeEvent(this.CHANGE_EVENT, this, detail)); 102 | } 103 | 104 | @listen({ 105 | event: 'uip:model:snippet:change', 106 | target: ($this: UIPRoot) => $this.model 107 | }) 108 | protected onSnippetChange({detail}: CustomEvent): void { 109 | this.$$fire(this.SNIPPET_CHANGE_EVENT, {detail, bubbles: false}); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /site/src/page/page.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | 13 | body { 14 | display: flex; 15 | flex-direction: column; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | hr { 21 | margin-top: 1rem; 22 | margin-bottom: 1rem; 23 | border: 0; 24 | border-top: 1px solid @section-border; 25 | } 26 | 27 | .main { 28 | position: relative; 29 | display: flex; 30 | height: 100%; 31 | flex-direction: column; 32 | flex: 1 1 auto; 33 | background-color: @light-color; 34 | 35 | &-title { 36 | margin: 10px auto; 37 | font-weight: 400; 38 | color: @dark-text; 39 | } 40 | 41 | &-desc { 42 | font-size: 20px; 43 | font-weight: lighter; 44 | color: @dark-text; 45 | 46 | p { 47 | margin-bottom: 0.5rem; 48 | } 49 | } 50 | 51 | &-content { 52 | width: 100%; 53 | } 54 | 55 | .main-container .uip-root { 56 | margin-top: 30px; 57 | 58 | .logo-content { 59 | display: flex; 60 | justify-content: center; 61 | flex-direction: column; 62 | align-items: center; 63 | min-height: 250px; 64 | height: 100%; 65 | 66 | .get-started svg { 67 | margin-inline-start: 0.5em; 68 | fill: currentColor; 69 | vertical-align: middle; 70 | } 71 | .get-started span { 72 | vertical-align: middle; 73 | } 74 | 75 | &.blue-clr { 76 | .get-started { 77 | border-color: @blue-color; 78 | color: @blue-color; 79 | svg { 80 | stroke: @blue-color; 81 | } 82 | } 83 | } 84 | 85 | &.gray-clr { 86 | .get-started { 87 | border-color: @landing-dark-bg; 88 | color: @landing-dark-bg; 89 | svg { 90 | stroke: @landing-dark-bg; 91 | } 92 | } 93 | } 94 | 95 | &.purple-clr { 96 | .get-started { 97 | border-color: @purple-color; 98 | color: @purple-color; 99 | svg { 100 | stroke: @purple-color; 101 | } 102 | } 103 | } 104 | } 105 | 106 | .get-started { 107 | display: flex; 108 | align-items: center; 109 | margin-top: 10px; 110 | padding: 0.5rem 2em; 111 | border-radius: 10px; 112 | font-size: 3rem; 113 | border: 4px solid @blue-color; 114 | color: @blue-color; 115 | } 116 | } 117 | } 118 | 119 | .content { 120 | flex: 1 0 auto; 121 | width: 100%; 122 | padding: 40px 0; 123 | 124 | ul, 125 | ol { 126 | list-style-position: inside; 127 | } 128 | } 129 | 130 | .container { 131 | width: 100%; 132 | margin-right: auto; 133 | margin-left: auto; 134 | padding-right: 60px; 135 | padding-left: 60px; 136 | 137 | @media @mobile { 138 | padding-right: 20px; 139 | padding-left: 20px; 140 | } 141 | 142 | @media @tablet { 143 | padding-right: 45px; 144 | padding-left: 45px; 145 | } 146 | } 147 | 148 | .footer { 149 | display: block; 150 | margin-top: auto; 151 | padding: 15px 0; 152 | color: @light-color; 153 | background-color: @landing-dark-bg; 154 | 155 | a { 156 | color: @light-color; 157 | } 158 | } 159 | 160 | .collection { 161 | &-title { 162 | font-weight: 400; 163 | color: @dark-text; 164 | font-size: 2rem; 165 | } 166 | 167 | &-list { 168 | margin: 20px 0; 169 | width: 100%; 170 | list-style-type: none; 171 | 172 | .title { 173 | margin: 10px 0; 174 | font-weight: 400; 175 | color: @dark-text; 176 | } 177 | 178 | .item { 179 | margin-bottom: 10px; 180 | border-bottom: 1px solid @section-border; 181 | 182 | a { 183 | color: @dark-text; 184 | } 185 | } 186 | } 187 | 188 | &-sublist { 189 | margin-left: 20px; 190 | list-style-type: none; 191 | } 192 | } 193 | 194 | .esl-scrollbar.page-scrollbar { 195 | top: @header-height-desktop; 196 | @media @mobile { 197 | top: @header-height-mobile; 198 | } 199 | } 200 | 201 | @import 'header.less'; 202 | @import 'sidebar.less'; 203 | -------------------------------------------------------------------------------- /src/core/base/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # UIPPlugin 4 | 5 | **UIPPlugin** - base class for all UIP elements. 6 | Should be used as a parent class for all custom plugins of UIP to correctly observe UIPRoot state. 7 | 8 | All UIP elements are **UIPPlugin** instances. Plugin automatically sets _uip-plugin_ class to its elements, 9 | provides access to [UIPRoot](src/core/README.md#uip-root). 10 | 11 | **UIPPlugin** has its own header section block, where additional buttons and header icon can be specified. 12 | 13 | **UIPPlugin** uses the following attributes: 14 | 15 | - **resizable** - allows resizing the plugin. 16 | - **collapsible** - allows collapsing the plugin. 17 | - **resizing** - indicates resizing state of the panel. 18 | - **collapsed** - collapses the plugin by default. 19 | - **vertical** - sets vertical orientation for the plugin. 20 | 21 | ## Processing markup changes 22 | 23 | ```typescript 24 | import {UIPPlugin} from './plugin'; 25 | 26 | class UIPComponent extends UIPPlugin { 27 | @listen({event: 'uip:change', target: (that: UIPSetting) => that.$root}) 28 | protected _onRootStateChange(): void { 29 | ... 30 | } 31 | } 32 | ``` 33 | 34 | You can find a way of getting current markup in [UIPStateModel](src/core/README.md#uip-state-model) section. 35 | 36 | ## Example 37 | 38 | ```typescript 39 | import {UIPPlugin} from './plugin'; 40 | 41 | class UIPPreview extends UIPPlugin { 42 | @listen({event: 'uip:change', target: (that: UIPPreview) => that.$root}) 43 | protected _onRootStateChange(): void { 44 | this.$inner.innerHTML = this.model!.html; 45 | this.innerHTML = ''; 46 | this.appendChild(this.$inner); 47 | } 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | 54 | 55 | # UIPRoot 56 | 57 | **UIPRoot** - container for **UIPPlugin** components. 58 | 59 | **UIPRoot** contains [UIPStateModel](src/core/README.md#uip-state-model) and [UIPSnippets](src/plugins/snippets/README.md) getters. It also allows **UIPPlugin** elements 60 | to subscribe to model, snippets or theme changes (or unsubscribe from them). More details can be found in [UIPPlugin](src/core/README.md#uip-plugin) section. 61 | 62 | ## Example 63 | 64 | ```html 65 | 66 | ``` 67 | 68 | --- 69 | 70 | 71 | 72 | # UIPStateModel 73 | 74 | **UIPStateModel** - state manager which contains current UIP state and provides methods for changing it. 75 | 76 | **UIPStateModel** has current js, markup and note states. 77 | It also provides [UIPSnippet](src/plugins/snippets/README.md) item values, current active snippet and snippet item that relates to current anchor. 78 | Every time we produce a change, it fires change event. 79 | 80 | **UIPStateModel** also contains the following methods: 81 | 82 | _getAttribute()_ - method returns attributes (_attr_ field) values from targets. 83 | 84 | _changeAttribute()_ - callback is used for changing elements attributes. It takes _ChangeAttrConfig_ as 85 | a parameter. This type looks like this: 86 | 87 | ```typescript 88 | export type TransformSignature = ( 89 | current: string | null 90 | ) => string | boolean | null; 91 | 92 | export type ChangeAttrConfig = { 93 | target: string; 94 | attribute: string; 95 | modifier: UIPPlugin; 96 | } & ( 97 | | { 98 | value: string | boolean; 99 | } 100 | | { 101 | transform: TransformSignature; 102 | } 103 | ); 104 | ``` 105 | 106 | Here _attribute_ stands for attribute name and _target_ - for target elements. _Modifier_ field represents the 107 | **UIPPlugin** instance which triggers attribute's changes. 108 | 109 | The last field can either be _value_ (this value replaces current _attribute_'s value) or _transform_ function (it maps 110 | current attribute value to the new one). 111 | 112 | The examples of using this API can be found in [UIPSetting](src/plugins/settings/README.md) implementations (e.g. [UIPBoolSetting](src/settings/bool-setting/README.md)). 113 | 114 | ## Example 115 | 116 | ```typescript 117 | import {UIPPlugin} from './plugin'; 118 | 119 | class UIPComponent extends UIPPlugin { 120 | protected _onComponentChange() { 121 | // ... 122 | this.model!.setHtml('New HTML here!', modifier); 123 | this.model!.setJS('New JS here!', modifier); 124 | this.model!.setNote('New Note here!', modifier); 125 | // ... 126 | } 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /src/plugins/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | 3 | import {debounce} from '@exadel/esl/modules/esl-utils/async/debounce'; 4 | import {attr, boolAttr, decorate, listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 5 | 6 | import {UIPPluginPanel} from '../../core/panel/plugin-panel'; 7 | import {ThemeToggleIcon} from '../theme/theme-toggle.icon'; 8 | 9 | import {SettingsIcon} from './settings.icon'; 10 | 11 | import type {UIPSetting} from './base-setting/base-setting'; 12 | 13 | /** 14 | * Settings {@link UIPPlugin} custom element definition 15 | * Container for {@link UIPSetting} 16 | */ 17 | export class UIPSettings extends UIPPluginPanel { 18 | public static is = 'uip-settings'; 19 | public static observedAttributes = ['dir-toggle', 'theme-toggle', ...UIPPluginPanel.observedAttributes]; 20 | 21 | /** Attribute to set all inner {@link UIPSetting} settings' {@link UIPSetting#target} targets */ 22 | @attr() public target: string; 23 | 24 | @boolAttr() public dirToggle: boolean; 25 | @boolAttr() public themeToggle: boolean; 26 | 27 | /** @readonly internal settings items state marker */ 28 | @boolAttr({readonly: true}) public inactive: boolean; 29 | 30 | protected override get $icon(): JSX.Element { 31 | return ; 32 | } 33 | 34 | @memoize() 35 | protected override get $toolbar(): HTMLElement { 36 | const type = this.constructor as typeof UIPSettings; 37 | return (
    38 | {this.themeToggle ? : ''} 39 | {this.dirToggle ? : ''} 40 |
    ) as HTMLElement; 41 | } 42 | 43 | @memoize() 44 | protected get $inner(): HTMLElement { 45 | const type = this.constructor as typeof UIPSettings; 46 | return (
    47 | 48 | {this.$container} 49 |
    ) as HTMLElement; 50 | } 51 | 52 | @memoize() 53 | protected get $container(): HTMLElement { 54 | const type = this.constructor as typeof UIPSettings; 55 | return (
    ) as HTMLElement; 56 | } 57 | 58 | @memoize() 59 | /** @returns HTMLElement[] - all internal items marked as settings item */ 60 | protected get $items(): UIPSetting[] { 61 | return [...this.querySelectorAll('[uip-settings-content]')] as UIPSetting[]; 62 | } 63 | 64 | /** @returns Element[] - active internal settings items */ 65 | protected get $activeItems(): Element[] { 66 | return this.$items.filter(($el: Element) => !$el.classList.contains('uip-inactive-setting')); 67 | } 68 | 69 | protected override connectedCallback(): void { 70 | super.connectedCallback(); 71 | this.appendChild(this.$header); 72 | this.appendChild(this.$inner); 73 | this.appendChild(this.$resize); 74 | this.invalidate(); 75 | } 76 | 77 | protected override disconnectedCallback(): void { 78 | super.disconnectedCallback(); 79 | this.append(...this.$items); 80 | this.removeChild(this.$header); 81 | this.removeChild(this.$inner); 82 | } 83 | 84 | protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { 85 | super.attributeChangedCallback(attrName, oldVal, newVal); 86 | if (['label', 'collapsible', 'dir-toggle', 'theme-toggle'].includes(attrName)) { 87 | this.$header.remove(); 88 | this.$toolbar.remove(); 89 | memoize.clear(this, ['$header', '$toolbar']); 90 | this.insertAdjacentElement('afterbegin', this.$header); 91 | } 92 | } 93 | 94 | @decorate(debounce, 100) 95 | protected invalidate(): void { 96 | memoize.clear(this, '$items'); 97 | const outside = this.$items.filter((el) => el.parentElement !== this.$container); 98 | outside.forEach((el) => this.$container.appendChild(el)); 99 | this.onSettingsStateChange(); 100 | } 101 | 102 | @listen('uip:settings:invalidate') 103 | protected onInvalidate(): void { 104 | this.invalidate(); 105 | } 106 | 107 | /** Handles internal settings items state change */ 108 | @listen('uip:settings:state:change') 109 | protected onSettingsStateChange(): void { 110 | this.$$attr('inactive', !this.$activeItems.length); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIPlayground 2 | 3 | > :warning: **Notice: This repository has moved** 4 | > Active development, issues, and pull requests are now handled in :point_right: [exadel-inc/esl](https://github.com/exadel-inc/esl). 5 | > This repository is archived and no longer maintained. 6 | 7 | **UIPlayground** is a solution for presenting your custom components. 8 | 9 | With the help of *UIP* components we allow user to *'play'* with a component. 10 | You can choose from the variety of component's templates ([UIP Snippets](src/plugins/snippets-list/README.md)), 11 | play with the component's settings ([UIP Settings](src/plugins/settings/README.md)) 12 | or even change its markup ([UIP Editor](src/plugins/editor/README.md))! 13 | 14 | Every element (except the *UIP Root*) isn't required, so you can combine them the way you want. 15 | 16 | --- 17 | ## Installation 18 | 19 | Install UIPlayground [npm dependency](https://www.npmjs.com/package/@exadel/ui-playground) 20 | ```bash 21 | npm i @exadel/ui-playground --save 22 | ``` 23 | Run initialization function 24 | ```javascript 25 | import {init} from '@exadel/ui-playground/esm/registration.js'; 26 | init(); 27 | ``` 28 | Import CSS styles 29 | ```css 30 | @import "@exadel/ui-playground/esm/registration.css"; 31 | ``` 32 | 33 | --- 34 | ## UIP elements 35 | 36 | - ### Core 37 | - #### [UIP Root](src/core/README.md#uip-root) 38 | - #### [UIP Preview](src/core/README.md) 39 | - ### Plugins 40 | - #### [UIP Editor](src/plugins/editor/README.md) 41 | - #### [UIP Settings and Setting](src/plugins/settings/README.md) 42 | - ##### [UIP Text Setting](src/plugins/settings/text-setting/README.md) 43 | - ##### [UIP Bool Setting](src/plugins/settings/bool-setting/README.md) 44 | - ##### [UIP Select Setting](src/plugins/settings/select-setting/README.md) 45 | - ##### [UIP Snippets](src/plugins/snippets/README.md) 46 | - ##### [UIP Snippets Title](src/plugins/snippets-title/README.md) 47 | - ##### [UIP Snippets List](src/plugins/snippets-list/README.md) 48 | - #### [UIP Theme Toggle](src/plugins/theme/README.md) 49 | - #### [UIP Note](src/plugins/note/README.md) 50 | - #### [UIP Copy](src/plugins/copy/README.md) 51 | - #### [UIP Text Direction Toggle](src/plugins/direction/README.md) 52 | --- 53 | ## Example 54 | 55 | ![Example](docs/images/UIPexample2.png) 56 | 57 | ```html 58 | 59 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | --- 84 | 85 | ## Roadmap 86 | - More demo content 87 | - UIPNote: design improvement and technical support to store a note (any HTML content) associated with snippet 88 | --- 89 | 90 | ## License 91 | 92 | Distributed under the MIT License. See [LICENSE](https://github.com/exadel-inc/ui-playground/blob/HEAD/CLA.md) 93 | for more information. 94 | 95 | --- 96 | 97 | **Exadel, Inc.** 98 | 99 | [![](docs/images/exadel-logo.png)](https://exadel.com) 100 | -------------------------------------------------------------------------------- /src/plugins/settings/base-setting/base-setting.ts: -------------------------------------------------------------------------------- 1 | import {attr, prop, listen} from '@exadel/esl/modules/esl-utils/decorators'; 2 | import {getAttr, setAttr} from '@exadel/esl/modules/esl-utils/dom/attr'; 3 | 4 | import {UIPPlugin} from '../../../core/base/plugin'; 5 | 6 | import type {UIPStateModel} from '../../../core/base/model'; 7 | import type {UIPChangeEvent} from '../../../core/base/model.change'; 8 | 9 | /** 10 | * Custom element for manipulating with elements attributes 11 | * Custom settings should extend this class 12 | * to become connected with {@link UIPSettings} 13 | */ 14 | export abstract class UIPSetting extends UIPPlugin { 15 | public static override is = 'uip-setting'; 16 | 17 | /** No matching value message */ 18 | @prop('No select match') public NO_MATCH_MSG: string; 19 | /** No target element message */ 20 | @prop('No setting target') public NO_TARGET_MSG: string; 21 | /** Inconsistent values found message */ 22 | @prop('Inconsistent value') public INCONSISTENT_VALUE_MSG: string; 23 | /** Multiple values found message */ 24 | @prop('Multiple values') public MULTIPLE_VALUE_MSG: string; 25 | /** Invalid value message */ 26 | @prop('Invalid setting value') public INVALID_VALUE_MSG: string; 27 | /** No value specified */ 28 | @prop('No value specified') public NOT_VALUE_SPECIFIED_MSG: string; 29 | 30 | /** Class for label field element */ 31 | @attr({defaultValue: 'label-field'}) public labelFieldClass: string; 32 | /** Class for label input element */ 33 | @attr({defaultValue: 'label-input'}) public labelInputClass: string; 34 | /** Class for label message element */ 35 | @attr({defaultValue: 'label-msg'}) public labelMsgClass: string; 36 | /** Class for inconsistent message element */ 37 | @attr({defaultValue: 'inconsistency-msg'}) public inconsistencyMsgClass: string; 38 | /** {@link target} attribute which is changed by setting */ 39 | @attr() public attribute: string; 40 | 41 | /** Target to which setting's changes are attached */ 42 | public get target(): string { 43 | const target = this.closest('[target]'); 44 | return target ? getAttr(target, 'target')! : ''; 45 | } 46 | /** Sets target to which setting's changes are attached */ 47 | public set target(target: string) { 48 | setAttr(this, 'target', target); 49 | } 50 | 51 | protected override connectedCallback(): void { 52 | this.$$attr('uip-settings-content', true); 53 | this.$$fire('uip:settings:invalidate'); 54 | super.connectedCallback(); 55 | this.classList.add(UIPSetting.is); 56 | this._onRootStateChange(); 57 | } 58 | 59 | /** 60 | * Handles setting value change and 61 | * dispatches `uip:change` event 62 | */ 63 | @listen('change') 64 | protected _onChange(e: Event): void { 65 | e.preventDefault(); 66 | if (!this.model) return; 67 | this.applyTo(this.model); 68 | } 69 | 70 | /** 71 | * Changes markup in {@link UIPStateModel} 72 | * with setting's value 73 | */ 74 | public applyTo(model: UIPStateModel): void { 75 | if (this.isValid()) { 76 | model.changeAttribute({ 77 | target: this.target, 78 | attribute: this.attribute, 79 | value: this.getDisplayedValue(), 80 | modifier: this 81 | }); 82 | } else { 83 | this.setInconsistency(this.INVALID_VALUE_MSG); 84 | } 85 | } 86 | 87 | /** 88 | * Updates setting's value with active markup from {@link UIPStateModel} 89 | */ 90 | public updateFrom(model: UIPStateModel): void { 91 | const values = model.getAttribute(this.target, this.attribute); 92 | this.classList.toggle('uip-inactive-setting', !values.length); 93 | this.setDisabled(!values.length); 94 | } 95 | 96 | /** Updates {@link UIPSetting} values */ 97 | @listen({event: 'uip:change', target: ($this: UIPSetting) => $this.$root}) 98 | protected _onRootStateChange(e?: UIPChangeEvent): void { 99 | if (e && !e.htmlChanges.length) return; 100 | this.updateFrom(this.model!); 101 | // TODO: throw only if real state change 102 | this.$$fire('uip:settings:state:change'); 103 | } 104 | 105 | /** 106 | * Checks whether setting's value is valid or not 107 | * Use for custom validation 108 | */ 109 | protected isValid(): boolean { 110 | return true; 111 | } 112 | 113 | /** 114 | * Indicates setting's incorrect state 115 | * (e.g. multiple attribute values or no target provided) 116 | */ 117 | protected setInconsistency(msg: string = this.INCONSISTENT_VALUE_MSG): void {} 118 | 119 | /** 120 | * Disable setting 121 | * By default is used when there are no setting's targets 122 | */ 123 | public setDisabled(force: boolean): void { 124 | this.$$attr('disabled', force); 125 | } 126 | 127 | /** 128 | * Gets setting's value 129 | * to update markup in {@link UIPStateModel} 130 | */ 131 | protected abstract getDisplayedValue(): string | boolean; 132 | 133 | /** 134 | * Sets setting's value 135 | * after processing markup in {@link UIPStateModel} 136 | */ 137 | protected abstract setValue(value: string | null): void; 138 | } 139 | -------------------------------------------------------------------------------- /src/plugins/settings/bool-setting/bool-setting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'jsx-dom'; 2 | import {attr, memoize} from '@exadel/esl/modules/esl-utils/decorators'; 3 | 4 | import {TokenListUtils} from '../../../core/utils/token-list'; 5 | import {UIPSetting} from '../base-setting/base-setting'; 6 | 7 | import type {ChangeAttrConfig, UIPStateModel} from '../../../core/base/model'; 8 | 9 | /** 10 | * Custom setting to add/remove attributes or append values to attribute 11 | */ 12 | export class UIPBoolSetting extends UIPSetting { 13 | public static override is = 'uip-bool-setting'; 14 | 15 | /** Setting's visible name */ 16 | @attr({defaultValue: ''}) public label: string; 17 | /** 18 | * Value for updating [attribute's]{@link UIPSetting#attribute} value 19 | * If it's unset, setting adds/removes [attribute]{@link UIPSetting#attribute} 20 | */ 21 | @attr({defaultValue: ''}) public value: string; 22 | /** 23 | * Attribute to set mode for setting 24 | * `replace` - replacing [attribute's]{@link UIPSetting#attribute} value with setting's value 25 | * `append` - appending [attribute's]{@link UIPSetting#attribute} value to attribute's value 26 | */ 27 | @attr({defaultValue: 'replace'}) public mode: 'replace' | 'append'; 28 | 29 | /** Checkbox field to change setting's value */ 30 | @memoize() 31 | protected get $field(): HTMLInputElement { 32 | const $field = document.createElement('input'); 33 | $field.type = 'checkbox'; 34 | $field.name = this.label; 35 | $field.className = this.labelInputClass; 36 | return $field; 37 | } 38 | 39 | @memoize() 40 | protected get $inner(): HTMLElement { 41 | return ( 42 | 45 | ) as HTMLElement; 46 | } 47 | 48 | /** Container element for displaying inconsistency message */ 49 | @memoize() 50 | protected get $inconsistencyMsg(): HTMLElement { 51 | return as HTMLElement; 52 | } 53 | 54 | protected override connectedCallback(): void { 55 | super.connectedCallback(); 56 | this.innerHTML = ''; 57 | this.appendChild(this.$inner); 58 | } 59 | 60 | applyTo(model: UIPStateModel): void { 61 | if (this.mode === 'replace') return super.applyTo(model); 62 | 63 | const cfg: ChangeAttrConfig = { 64 | target: this.target, 65 | attribute: this.attribute, 66 | modifier: this, 67 | transform: this.transform.bind(this, this.getDisplayedValue()), 68 | }; 69 | 70 | model.changeAttribute(cfg); 71 | } 72 | 73 | /** Function to transform(update) attribute value */ 74 | transform(value: string | false, attrValue: string | null): string | null { 75 | if (!attrValue) return value || null; 76 | 77 | const attrTokens = TokenListUtils.remove(TokenListUtils.split(attrValue), this.value); 78 | value && attrTokens.push(this.value); 79 | 80 | return TokenListUtils.join(attrTokens); 81 | } 82 | 83 | updateFrom(model: UIPStateModel): void { 84 | super.updateFrom(model); 85 | const values = model.getAttribute(this.target, this.attribute); 86 | if (!values.length) { 87 | this.setInconsistency(this.NO_TARGET_MSG); 88 | } else { 89 | this.mode === 'replace' ? this.updateReplace(values) : this.updateAppend(values); 90 | } 91 | } 92 | 93 | /** Updates setting's value for replace {@link mode} */ 94 | protected updateReplace(attrValues: (string | null)[]): void { 95 | if (!TokenListUtils.isAllEqual(attrValues)) { 96 | return this.setInconsistency(this.MULTIPLE_VALUE_MSG); 97 | } 98 | 99 | return this.setValue((this.value && attrValues[0] !== this.value) ? null : attrValues[0]); 100 | } 101 | 102 | /** Updates setting's value for append {@link mode} */ 103 | protected updateAppend(attrValues: (string | null)[]): void { 104 | const containsFunction = (val: string | null) => 105 | TokenListUtils.contains(TokenListUtils.split(val), [this.value]); 106 | 107 | if (attrValues.every(containsFunction)) return this.setValue(this.value); 108 | if (!attrValues.some(containsFunction)) return this.setValue(null); 109 | 110 | return this.setInconsistency(this.MULTIPLE_VALUE_MSG); 111 | } 112 | 113 | protected getDisplayedValue(): string | false { 114 | if (this.value) { 115 | return this.$field.checked ? this.value : false; 116 | } 117 | 118 | return this.$field.checked ? '' : false; 119 | } 120 | 121 | protected setValue(value: string | null): void { 122 | if (this.value) { 123 | this.$field.checked = value === this.value; 124 | } else { 125 | this.$field.checked = value !== null; 126 | } 127 | this.$inconsistencyMsg.remove(); 128 | } 129 | 130 | protected setInconsistency(msg = this.INCONSISTENT_VALUE_MSG): void { 131 | this.$field.checked = false; 132 | this.$inconsistencyMsg.innerText = msg; 133 | this.$inner.append(this.$inconsistencyMsg); 134 | } 135 | 136 | public setDisabled(force: boolean): void { 137 | this.$inconsistencyMsg.classList.toggle('disabled', force); 138 | this.$field.toggleAttribute('disabled', force); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /site/views/examples/example/list.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example with list of cards 3 | --- 4 | 5 | 6 |
    7 | 8 | 9 |
    10 | 65 | 76 | 83 | 90 | 97 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
    120 | -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Contributor License Agreement 2 | 3 | Thank you for your interest in contributing to the UI Playground ("Material") by Exadel, Inc. ("We" or "Us"). The present Contributor License Agreement ("CLA") is for your protection as a Contributor as well as the protection of Us; it does not change your rights to use your own Contributions for any other purpose. 4 | 5 | You must agree to the terms of this CLA before making a Contribution to the Material. This CLA covers any and all Contributions that You, now or in the future, submit to the Material. This CLA shall come into effect upon Your acceptance of its terms and conditions. 6 | 7 | ## 1. Definitions 8 | a. "You" means the individual Copyright owner who Submits a Contribution to Us. 9 | 10 | b. "Contribution" means source code and any other copyrightable materials submitted by you to Us, including any associated comments and documentation. 11 | 12 | c. "Copyright" means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence. 13 | 14 | d. "Material" means the software or documentation made available by Us to third parties. After You Submit the Contribution, it may be included in the Material. 15 | 16 | e. "Submit" means any act by which a Contribution is transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 17 | 18 | ## 2. Grant of Copyright License 19 | 20 | Subject to the terms and conditions of this CLA, You hereby grant to Us and to recipients of Material distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 21 | 22 | ## 3. Grant of Patent License 23 | 24 | Subject to the terms and conditions of this CLA, You hereby grant to Us and to recipients of Material distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Material, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by a combination of Your Contribution(s) with the Material to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Material to which you have contributed, constitutes a direct or contributory patent infringement, then any patent licenses granted to that entity under this CLA for that Contribution or Material shall terminate as of the date such litigation is filed. 25 | 26 | ## 4. Other rights reserved 27 | 28 | Each party reserves all rights not expressly granted in this CLA. No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are granted by implication, exhaustion, estoppel or otherwise. 29 | 30 | ## 5. Originality of Contributions 31 | 32 | You represent that you are legally entitled to grant the above licenses. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer or that your employer has waived such rights for your Contributions to Us. You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 33 | 34 | ## 6. Notice to Us 35 | 36 | You agree to notify Us of any facts or circumstances of which you become aware that would make the representations in this CLA inaccurate in any respect. 37 | 38 | ## 7. Disclaimer 39 | 40 | You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on "as is" basis. More particularly, all express or implied warranties including, without limitation, any implied warranty of satisfactory quality, fitness for a particular purpose, and non-infringement are expressly disclaimed by You to Us and by Us to You. To the extent that any such warranties cannot be disclaimed, such warranty is limited in duration and extent to the minimum period and extent permitted by applicable law. 41 | 42 | ## 8. Consequential Damage Waiver 43 | 44 | To the maximum extent permitted by applicable law, in no event will You or We be liable for any loss of profits, loss of anticipated savings, loss of data, indirect, special, incidental, consequential and exemplary damages arising out of this CLA regardless of the legal or equitable theory (contract, tort or otherwise) upon which the claim is based. 45 | 46 | ## 9. Information About Submissions 47 | 48 | You agree that this Material and Contributions to it are public and that a record of the Contribution (including all personal information you submit with it) is maintained indefinitely and may be redistributed consistent with this Material, compliance with the open source license(s) involved, and maintenance of authorship attribution. 49 | 50 | ## 10. Miscellaneous 51 | 52 | This CLA is the entire agreement between the parties and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. You acknowledge that We are not obligated to use your Contribution as part of the Material distributed by Us and may make the decision to include any Contribution as We believe is appropriate. 53 | --------------------------------------------------------------------------------