├── .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 |
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 | Dark gray
25 | Blue
26 | Purple
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 | Dark gray
25 | Blue
26 | Purple
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 |
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 | 64x64
18 | 128x128
19 | 256x256
20 | None
21 |
22 |
23 | Vertical
24 | Horizontal
25 |
26 |
27 | Align items
28 | Center content
29 | Wrap items
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 =
28 | {this.label}
29 | {this.$field}
30 | ;
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 | Dark gray
34 | Blue
35 | Purple
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 |
26 | {this.label}
27 | {this.$field}
28 |
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 | Auto mode
51 | Cover mode
52 | Inscribe mode
53 |
54 |
55 | Italic
56 | Bold
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 | Red
27 | Aqua
28 | Green
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 | Red
69 | Aqua
70 | Green
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 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 | {snippet.label}
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 | 
56 |
57 | ```html
58 |
59 |
70 |
71 |
72 |
73 |
74 | Dark gray
75 | Blue
76 | Purple
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 | [](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 |
43 | {this.$field}{this.label}
44 |
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 | Red
108 | Aqua
109 | Green
110 |
111 |
112 | Top
113 | Center
114 | Bottom
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 |
--------------------------------------------------------------------------------