├── .github └── workflows │ ├── github_pages.yml │ └── main.yml ├── .gitignore ├── .postcssrc ├── .posthtmlrc ├── .prettierignore ├── .prettierrc ├── .yarnrc.yml ├── BUILD.sh ├── CHANGELOG.md ├── README.md ├── cypress.config.js ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── components │ │ ├── button.spec.ts │ │ ├── input.spec.ts │ │ ├── item.spec.ts │ │ ├── list.spec.ts │ │ ├── repeater.spec.ts │ │ └── tree.spec.ts │ └── unit │ │ └── core │ │ ├── attach.ts │ │ ├── compile.ts │ │ └── event.ts ├── plugins │ └── index.ts ├── support │ ├── commands.js │ ├── commands.ts │ ├── e2e.js │ └── index.ts └── tsconfig.json ├── eslint.config.js ├── package.json ├── src ├── client │ ├── app │ │ ├── component │ │ │ ├── button.ts │ │ │ ├── code.ts │ │ │ ├── counter.ts │ │ │ ├── grid.ts │ │ │ ├── headline.ts │ │ │ ├── hello.state.ts │ │ │ ├── hello.ts │ │ │ ├── input.ts │ │ │ ├── item.ts │ │ │ ├── list.ts │ │ │ ├── logo.ts │ │ │ ├── main-nav.ts │ │ │ ├── meter.ts │ │ │ ├── side-nav.ts │ │ │ ├── stats.ts │ │ │ └── tree │ │ │ │ ├── atom.ts │ │ │ │ ├── node.ts │ │ │ │ └── tree.ts │ │ ├── index.ts │ │ ├── routing.ts │ │ └── view │ │ │ ├── 404 │ │ │ ├── 404.css │ │ │ ├── 404.html │ │ │ ├── 404.ts │ │ │ └── index.ts │ │ │ ├── home │ │ │ ├── home.css │ │ │ ├── home.html │ │ │ ├── home.ts │ │ │ └── index.ts │ │ │ ├── lib │ │ │ ├── docs.ts │ │ │ ├── index.ts │ │ │ ├── lib.css │ │ │ ├── lib.html │ │ │ └── lib.ts │ │ │ ├── perf │ │ │ ├── index.ts │ │ │ ├── perf.css │ │ │ ├── perf.html │ │ │ └── perf.ts │ │ │ ├── query │ │ │ ├── index.ts │ │ │ ├── query.css │ │ │ ├── query.html │ │ │ └── query.ts │ │ │ └── test │ │ │ ├── index.ts │ │ │ ├── test.css │ │ │ ├── test.html │ │ │ └── test.ts │ ├── custom.html │ ├── favicon.ico │ ├── hello-state.html │ ├── hello.html │ ├── hello.ts │ ├── index.html │ ├── index.ts │ ├── performance.html │ ├── polyfill.ts │ ├── robots.txt │ ├── style │ │ ├── fonts │ │ │ ├── DankMono-Italic │ │ │ │ ├── DankMono-Italic.eot │ │ │ │ ├── DankMono-Italic.svg │ │ │ │ ├── DankMono-Italic.ttf │ │ │ │ ├── DankMono-Italic.woff │ │ │ │ └── fonts.css │ │ │ └── DankMono-Regular │ │ │ │ ├── DankMono-Regular.eot │ │ │ │ ├── DankMono-Regular.svg │ │ │ │ ├── DankMono-Regular.ttf │ │ │ │ ├── DankMono-Regular.woff │ │ │ │ └── fonts.css │ │ ├── readymade-ui.css │ │ └── style.css │ ├── tsconfig.json │ ├── typings.d.ts │ └── vendor.ts ├── modules │ ├── core │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── component │ │ │ └── index.ts │ │ ├── decorator │ │ │ └── index.ts │ │ ├── element │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── attach.ts │ │ │ │ ├── compile.ts │ │ │ │ └── util.ts │ │ ├── event │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── tsconfig.json │ ├── dom │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── custom │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── repeatr │ │ │ └── index.ts │ │ ├── rollup.config.js │ │ └── tsconfig.json │ ├── router │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── tsconfig.json │ ├── transmit │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── tsconfig.json │ └── ui │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── component │ │ ├── control.ts │ │ ├── index.ts │ │ ├── input │ │ │ ├── button.ts │ │ │ ├── buttonpad.ts │ │ │ ├── checkbox.ts │ │ │ ├── dial.ts │ │ │ ├── input.ts │ │ │ ├── radio.ts │ │ │ ├── select.ts │ │ │ ├── slider.ts │ │ │ ├── switch.ts │ │ │ └── textarea.ts │ │ └── surface │ │ │ ├── display-input.ts │ │ │ ├── element.ts │ │ │ ├── index.ts │ │ │ └── surface.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── tsconfig.json └── server │ ├── config.ts │ ├── index.ts │ ├── middleware │ └── ssr.ts │ ├── server.ts │ ├── shim.ts │ └── tsconfig.json ├── tsconfig.json ├── vite-env.d.ts ├── vite.config.hello.js ├── vite.config.index.js ├── vite.config.inline.js ├── vite.config.js ├── vite.config.routes.js ├── vite.config.server.js └── yarn.lock /.github/workflows/github_pages.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4.2.0 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: '20' 16 | 17 | - name: Enable Corepack 18 | run: corepack enable 19 | 20 | - name: Install Yarn 21 | run: corepack prepare yarn@4.5.0 --activate 22 | 23 | - name: Install Dependencies 24 | run: yarn install --immutable 25 | 26 | - name: Run Lint 27 | run: yarn lint 28 | 29 | - name: Check Format 30 | run: yarn pretty:check 31 | 32 | - name: Cypress 33 | uses: cypress-io/github-action@v6.7.6 34 | with: 35 | build: yarn build 36 | start: yarn serve 37 | wait-on: http://localhost:4444 38 | 39 | - name: Clean 40 | run: yarn clean:dist 41 | 42 | - name: Build 43 | run: yarn build:client 44 | 45 | - name: Deploy to GitHub Pages 46 | uses: peaceiris/actions-gh-pages@v3 47 | with: 48 | github_token: '${{ secrets.GITHUB_TOKEN }}' 49 | publish_dir: ./dist/client 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: [main] 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4.2.0 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: '20' 16 | 17 | - name: Enable Corepack 18 | run: corepack enable 19 | 20 | - name: Install Yarn 21 | run: corepack prepare yarn@4.5.0 --activate 22 | 23 | - name: Install Dependencies 24 | run: yarn install --immutable 25 | 26 | - name: Run Lint 27 | run: yarn lint 28 | 29 | - name: Check Format 30 | run: yarn pretty:check 31 | 32 | - name: Cypress 33 | uses: cypress-io/github-action@v6.7.6 34 | with: 35 | build: yarn build 36 | start: yarn serve 37 | wait-on: http://localhost:4444 38 | 39 | - name: Clean 40 | run: yarn clean:dist 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nova 3 | .cache 4 | .parcel-cache 5 | .DS_Store 6 | .rpt2_cache 7 | .vscode 8 | node_modules 9 | .out/ 10 | dist/ 11 | out-tsc/ 12 | cypress/videos 13 | cypress/screenshots 14 | packages/ 15 | .yarn -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": false, 3 | "plugins": { 4 | "cssnano": { 5 | "default": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.posthtmlrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": {} 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/client/app/view/home/home.html 2 | src/client/app/view/home/home.ts 3 | src/client/app/view/lib/lib.html -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /BUILD.sh: -------------------------------------------------------------------------------- 1 | npx tsc -p src/modules/core/tsconfig.json --outDir dist/packages/@readymade/core/esm2022 --declarationDir dist/packages/@readymade/core/typings 2 | npx rollup -c src/modules/core/rollup.config.js 3 | cp src/modules/core/package.json dist/packages/@readymade/core/package.json 4 | cp CHANGELOG.md dist/packages/@readymade/core/CHANGELOG.md 5 | cp src/modules/core/LICENSE.txt dist/packages/@readymade/core/LICENSE.txt 6 | cp src/modules/core/README.md dist/packages/@readymade/core/README.md 7 | 8 | 9 | npx tsc -p src/modules/dom/tsconfig.json --outDir dist/packages/@readymade/dom/esm2022 --declarationDir dist/packages/@readymade/dom/typings 10 | npx rollup -c src/modules/dom/rollup.config.js 11 | cp src/modules/dom/package.json dist/packages/@readymade/dom/package.json 12 | cp CHANGELOG.md dist/packages/@readymade/dom/CHANGELOG.md 13 | cp src/modules/dom/LICENSE.txt dist/packages/@readymade/dom/LICENSE.txt 14 | cp src/modules/dom/README.md dist/packages/@readymade/dom/README.md 15 | 16 | 17 | npx tsc -p src/modules/router/tsconfig.json --outDir dist/packages/@readymade/router/esm2022 --declarationDir dist/packages/@readymade/router/typings 18 | npx rollup -c src/modules/router/rollup.config.js 19 | cp src/modules/router/package.json dist/packages/@readymade/router/package.json 20 | cp CHANGELOG.md dist/packages/@readymade/router/CHANGELOG.md 21 | cp src/modules/router/LICENSE.txt dist/packages/@readymade/router/LICENSE.txt 22 | cp src/modules/router/README.md dist/packages/@readymade/router/README.md 23 | 24 | 25 | npx tsc -p src/modules/ui/tsconfig.json --outDir dist/packages/@readymade/ui/esm2022 --declarationDir dist/packages/@readymade/ui/typings 26 | npx rollup -c src/modules/ui/rollup.config.js 27 | cp src/modules/ui/package.json dist/packages/@readymade/ui/package.json 28 | cp CHANGELOG.md dist/packages/@readymade/ui/CHANGELOG.md 29 | cp src/modules/ui/LICENSE.txt dist/packages/@readymade/ui/LICENSE.txt 30 | cp src/modules/ui/README.md dist/packages/@readymade/ui/README.md 31 | cp src/client/style/readymade-ui.css dist/packages/@readymade/ui/readymade-ui.css 32 | 33 | npx tsc -p src/modules/transmit/tsconfig.json --outDir dist/packages/@readymade/transmit/esm2022 --declarationDir dist/packages/@readymade/transmit/typings 34 | npx rollup -c src/modules/transmit/rollup.config.js 35 | cp CHANGELOG.md dist/packages/@readymade/transmit/CHANGELOG.md 36 | cp src/modules/transmit/package.json dist/packages/@readymade/transmit/package.json 37 | cp src/modules/transmit/LICENSE.txt dist/packages/@readymade/transmit/LICENSE.txt 38 | cp src/modules/transmit/README.md dist/packages/@readymade/transmit/README.md 39 | 40 | 41 | rm -rf dist/packages/@readymade/core/fesm2022/typings 42 | rm -rf dist/packages/@readymade/dom/fesm2022/typings 43 | rm -rf dist/packages/@readymade/router/fesm2022/typings 44 | rm -rf dist/packages/@readymade/transmit/fesm2022/typings 45 | rm -rf dist/packages/@readymade/ui/fesm2022/typings 46 | rm -rf dist/packages/@readymade/dom/esm2022/core 47 | rm -rf dist/packages/@readymade/dom/typings/core 48 | rm -rf dist/packages/@readymade/router/esm2022/core 49 | rm -rf dist/packages/@readymade/router/typings/core 50 | rm -rf dist/packages/@readymade/ui/esm2022/core 51 | rm -rf dist/packages/@readymade/ui/esm2022/dom 52 | rm -rf dist/packages/@readymade/ui/typings/core 53 | rm -rf dist/packages/@readymade/ui/typings/dom 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # readymade 2 | 3 | JavaScript microlibrary for developing Web Components with Decorators that uses only native spec to provide robust features. 4 | 5 | - 🎰 Declare metadata for CSS and HTML ShadowDOM template 6 | - ☕️ Single interface for 'autonomous custom elements' and 'customized built-in elements' 7 | - 🏋️‍ Weighing in ~1Kb for 'Hello World' (gzipped) 8 | - 🎤 Event Emitter pattern 9 | - 1️⃣ One-way data binding 10 | - 🖥 Server side renderable 11 | - 🌲 Treeshakable 12 | 13 | Chat with us on [Dischord](https://discord.gg/xzsnBfD3fu). 14 | 15 | For more information, read the [Readymade documentation](https://readymade-ui.github.io/readymade). 16 | 17 | ### Getting Started 18 | 19 | Install Readymade: 20 | 21 | ``` 22 | npm install @readymade/core 23 | ``` 24 | 25 | If you want to develop with customized built-in elements or Readymade's Repeater components: 26 | 27 | ``` 28 | npm install @readymade/dom 29 | ``` 30 | 31 | If you want to use the client-side router: 32 | 33 | ``` 34 | npm install @readymade/router 35 | ``` 36 | 37 | For the UI library: 38 | 39 | ``` 40 | npm install @readymade/ui 41 | ``` 42 | 43 | ### Development 44 | 45 | This repo includes a development server built with Vite. 46 | 47 | Fork and clone the repo. Install dependencies with yarn. 48 | 49 | ``` 50 | yarn install 51 | ``` 52 | 53 | To develop, run `yarn start`. This will spin up a Vite development server at http://localhost:4443. 54 | 55 | For working on the documentation portal use `yarn start:client`. 56 | 57 | Production is built with `yarn build`. This will generate a client and server that can be deployed. 58 | 59 | For unit and e2e tests, run `yarn build` then `yarn test`. 60 | 61 | Use `yarn test:open` to open a GUI and run tests interactively. 62 | 63 | ### Production 64 | 65 | To build the library for production, i.e. to use as a local dependency in another project run `yarn build:library`. 66 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | e2e: { 3 | baseUrl: 'http://localhost:4444', 4 | includeShadowDOM: true, 5 | specPattern: 'cypress/integration/**/*.spec.{js,jsx,ts,tsx}', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/components/button.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('MyButtonComponent Test', () => { 4 | beforeEach(() => { 5 | cy.visit('/test'); 6 | cy.wait(1); 7 | }); 8 | 9 | it('Displays outline when clicked', () => { 10 | cy.get('app-testbed').shadow().find('button[is="my-button"]').click(); 11 | cy.get('app-testbed') 12 | .shadow() 13 | .find('button[is="my-button"]') 14 | .should('have.css', 'box-shadow', 'rgb(255, 105, 180) 0px 0px 0px 0px'); 15 | }); 16 | 17 | it('Displays Click', () => { 18 | cy.get('app-testbed') 19 | .shadow() 20 | .find('button[is="my-button"]') 21 | .contains('Click'); 22 | }); 23 | 24 | it('Controls MyListComponent with BroadcastChannel API', () => { 25 | cy.get('app-testbed').shadow().find('button[is="my-button"]').click(); 26 | cy.get('app-testbed') 27 | .shadow() 28 | .find('my-item') 29 | .invoke('attr', 'state') 30 | .should('contain', '--selected'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /cypress/integration/components/input.spec.ts: -------------------------------------------------------------------------------- 1 | describe('MyInputComponent Test', () => { 2 | beforeEach(() => { 3 | cy.visit('/test'); 4 | cy.wait(1); 5 | }); 6 | 7 | it('Displays input when focused', () => { 8 | cy.get('app-testbed') 9 | .shadow() 10 | .find('input[is="my-input"]') 11 | .focus() 12 | .invoke('val') 13 | .should('contain', 'input'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /cypress/integration/components/item.spec.ts: -------------------------------------------------------------------------------- 1 | describe('MyItemComponent Test', () => { 2 | beforeEach(() => { 3 | cy.visit('/test'); 4 | cy.wait(1); 5 | }); 6 | 7 | it('Displays the item message', () => { 8 | cy.get('app-testbed').shadow().find('my-item').first().contains('Item 1'); 9 | }); 10 | 11 | it('Displays selected when clicked', () => { 12 | cy.get('app-testbed') 13 | .shadow() 14 | .find('my-item') 15 | .first() 16 | .click('left') 17 | .invoke('attr', 'state') 18 | .should('contain', '--selected'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /cypress/integration/components/list.spec.ts: -------------------------------------------------------------------------------- 1 | describe('MyListComponent Test', () => { 2 | beforeEach(() => { 3 | cy.visit('/test'); 4 | }); 5 | 6 | it('Displays four instances of MyItemComponent', () => { 7 | cy.get('app-testbed') 8 | .shadow() 9 | .find('my-list') 10 | .find('my-item') 11 | .should('have.length', 4); 12 | }); 13 | 14 | it('Selects the last item when clicked', () => { 15 | cy.get('app-testbed') 16 | .shadow() 17 | .find('my-list') 18 | .find('li') 19 | .first() 20 | .click('left'); 21 | cy.get('app-testbed') 22 | .shadow() 23 | .find('my-list') 24 | .find('li') 25 | .last() 26 | .click('left'); 27 | cy.get('app-testbed') 28 | .shadow() 29 | .find('my-list') 30 | .find('my-item') 31 | .last() 32 | .invoke('attr', 'state') 33 | .should('contain', '--selected'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /cypress/integration/components/repeater.spec.ts: -------------------------------------------------------------------------------- 1 | describe('TemplateRepeater Test', () => { 2 | beforeEach(() => { 3 | cy.visit('/test'); 4 | cy.wait(100); 5 | }); 6 | 7 | it('Displays TemplateRepeater', () => { 8 | cy.get('app-testbed').shadow().find('r-repeatr').should('exist'); 9 | }); 10 | 11 | it('Displays r-repeatr from existing set by object', () => { 12 | cy.get('app-testbed') 13 | .shadow() 14 | .find('r-repeatr') 15 | .first() 16 | .find('li') 17 | .last() 18 | .contains('Item 5'); 19 | }); 20 | 21 | it('Displays r-repeatr from existing set by array', () => { 22 | cy.get('app-testbed') 23 | .shadow() 24 | .find('r-repeatr') 25 | .last() 26 | .find('li') 27 | .last() 28 | .contains('five'); 29 | }); 30 | 31 | it('Displays template from r-repeat set by object', () => { 32 | cy.get('app-testbed') 33 | .shadow() 34 | .find('ul.is--large') 35 | .first() 36 | .find('li') 37 | .last() 38 | .contains('Item 5'); 39 | }); 40 | 41 | it('Displays template from r-repeat set by array', () => { 42 | cy.get('app-testbed') 43 | .shadow() 44 | .find('ul.is--large') 45 | .last() 46 | .find('li') 47 | .last() 48 | .contains('five'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /cypress/integration/components/tree.spec.ts: -------------------------------------------------------------------------------- 1 | describe('TreeComponent Test', () => { 2 | beforeEach(() => { 3 | cy.visit('/test'); 4 | cy.wait(100); 5 | }); 6 | 7 | it('Displays TreeComponent', () => { 8 | cy.get('app-testbed').shadow().find('x-tree').should('exist'); 9 | }); 10 | 11 | it('Displays text set by array', () => { 12 | cy.get('app-testbed') 13 | .shadow() 14 | .find('x-tree') 15 | .shadow() 16 | .find('x-node') 17 | .first() 18 | .shadow() 19 | .find('x-atom') 20 | .shadow() 21 | .contains('aaa'); 22 | }); 23 | 24 | it('Displays text set by nested array', () => { 25 | cy.get('app-testbed') 26 | .shadow() 27 | .find('x-tree') 28 | .shadow() 29 | .find('x-node') 30 | .eq(1) 31 | .shadow() 32 | .find('x-atom') 33 | .shadow() 34 | .contains('fiz'); 35 | }); 36 | 37 | it('Displays text set by dot syntax', () => { 38 | cy.get('app-testbed') 39 | .shadow() 40 | .find('x-tree') 41 | .shadow() 42 | .find('x-node') 43 | .eq(2) 44 | .shadow() 45 | .find('x-atom') 46 | .shadow() 47 | .contains('bbb'); 48 | }); 49 | 50 | it('Displays text set by bracket notation', () => { 51 | cy.get('app-testbed') 52 | .shadow() 53 | .find('x-tree') 54 | .shadow() 55 | .find('x-node') 56 | .eq(3) 57 | .shadow() 58 | .find('x-atom') 59 | .shadow() 60 | .contains('fuz'); 61 | }); 62 | 63 | it('Displays text set by shallow property', () => { 64 | cy.get('app-testbed') 65 | .shadow() 66 | .find('x-tree') 67 | .shadow() 68 | .find('x-node') 69 | .eq(4) 70 | .shadow() 71 | .find('x-atom') 72 | .shadow() 73 | .contains('ddd'); 74 | }); 75 | 76 | it('Displays text set by string in setState()', () => { 77 | cy.get('app-testbed') 78 | .shadow() 79 | .find('x-tree') 80 | .shadow() 81 | .find('x-node') 82 | .last() 83 | .shadow() 84 | .find('x-atom') 85 | .shadow() 86 | .contains('deep'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /cypress/integration/unit/core/attach.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | attachShadow, 4 | attachDOM, 5 | attachStyle, 6 | } from '../../../../src/modules/core/element/src/attach'; 7 | import { ElementMeta } from './../../../../src/modules/core/decorator'; 8 | 9 | interface ReadymadeElement extends HTMLElement { 10 | bindTemplate?: () => void; 11 | template?: string; 12 | elementMeta?: ElementMeta; 13 | } 14 | 15 | let element: ReadymadeElement; 16 | 17 | describe('attachShadow Test', () => { 18 | beforeEach(() => { 19 | element = document.createElement('div'); 20 | element.bindTemplate = () => {}; 21 | element.template = ` 22 |
Readymade Test
23 | `; 24 | element.elementMeta = { 25 | selector: 'x-test', 26 | mode: 'open', 27 | }; 28 | }); 29 | 30 | it('binds shadow root to element', () => { 31 | attachShadow(element, { mode: element.elementMeta.mode }); 32 | expect(element.shadowRoot).does.not.equal(null); 33 | }); 34 | 35 | it('has a shadow dom template', () => { 36 | attachShadow(element, { mode: element.elementMeta.mode }); 37 | expect(element.shadowRoot.querySelector('div').innerText).equals( 38 | 'Readymade Test', 39 | ); 40 | }); 41 | }); 42 | 43 | describe('attachDOM Test', () => { 44 | beforeEach(() => { 45 | element = document.createElement('div'); 46 | element.bindTemplate = () => {}; 47 | element.elementMeta = { 48 | selector: 'x-test', 49 | template: ` 50 |
Readymade Test
51 | `, 52 | }; 53 | }); 54 | 55 | it('does not bind shadow root to element', () => { 56 | expect(element.shadowRoot).equals(null); 57 | }); 58 | 59 | it('has a template', () => { 60 | attachDOM(element); 61 | expect(element.querySelector('div').innerText).equals('Readymade Test'); 62 | }); 63 | }); 64 | 65 | describe('attachStyle Test', () => { 66 | beforeEach(() => { 67 | element = document.createElement('div'); 68 | element.bindTemplate = () => {}; 69 | element.elementMeta = { 70 | selector: 'x-test', 71 | template: ` 72 |
Readymade Test
73 | `, 74 | style: ` 75 | :host { 76 | background: #ff0000; 77 | } 78 | `, 79 | }; 80 | }); 81 | 82 | xit('head contains style tag with injected styles', () => { 83 | attachShadow(element, { mode: 'open' }); 84 | attachStyle(element); 85 | const style: HTMLElement = document 86 | .querySelector('head') 87 | .querySelector('[id="x-test-x"]'); 88 | expect(style.innerText).contains('[is=x-test]'); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /cypress/integration/unit/core/compile.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | isObject, 4 | findValueByString, 5 | setValueByString, 6 | uuidv4, 7 | templateId, 8 | compileTemplate, 9 | } from '../../../../src/modules/core/element/src/compile'; 10 | import { ElementMeta } from './../../../../src/modules/core/decorator'; 11 | 12 | interface ReadymadeElement extends HTMLElement { 13 | template?: string; 14 | elementMeta?: ElementMeta; 15 | bindTemplate?: () => void; 16 | setState?: () => void; 17 | } 18 | 19 | let element: ReadymadeElement; 20 | 21 | describe('Compile Test', () => { 22 | beforeEach(() => { 23 | element = document.createElement('div'); 24 | element.bindTemplate = () => {}; 25 | element.template = ` 26 |
Readymade Test
27 | `; 28 | }); 29 | 30 | it('identifies object', () => { 31 | const obj = { 32 | foo: 'bar', 33 | }; 34 | expect(isObject(obj)).equal(true); 35 | }); 36 | 37 | it('identifies function', () => { 38 | const obj = () => {}; 39 | expect(isObject(obj)).equal(true); 40 | }); 41 | 42 | it('rejects string', () => { 43 | const obj = 'fizz'; 44 | expect(isObject(obj)).equal(false); 45 | }); 46 | 47 | it('rejects number', () => { 48 | const obj = 4; 49 | expect(isObject(obj)).equal(false); 50 | }); 51 | 52 | it('finds a value in object', () => { 53 | const obj = { 54 | foo: { 55 | bar: { 56 | baz: 'bravo', 57 | }, 58 | }, 59 | }; 60 | expect(findValueByString(obj, 'foo.bar.baz')).equal('bravo'); 61 | }); 62 | 63 | it('finds a value in mixed object', () => { 64 | let obj = { 65 | foo: { 66 | bar: [ 67 | { 68 | baz: 'bravo', 69 | }, 70 | ], 71 | }, 72 | }; 73 | expect(findValueByString(obj, 'foo.bar[0].baz')).equal('bravo'); 74 | }); 75 | 76 | it('finds a value in nested array', () => { 77 | let arr = [ 78 | [ 79 | { 80 | baz: 'bravo', 81 | }, 82 | ], 83 | ]; 84 | expect(findValueByString(arr, '[0][0].baz')).equal('bravo'); 85 | }); 86 | 87 | it('creates a uuid', () => { 88 | const obj = { 89 | foo: { 90 | bar: { 91 | baz: 'bravo', 92 | }, 93 | }, 94 | }; 95 | setValueByString(obj, 'foo.bar.baz', 'zulu'); 96 | expect(findValueByString(obj, 'foo.bar.baz')).equal('zulu'); 97 | }); 98 | 99 | it('creates template id', () => { 100 | const id = templateId(); 101 | const regex = /([a-z]{3})/; 102 | console.log(id); 103 | expect(regex.test(id)).equal(true); 104 | }); 105 | 106 | it('creates uuid', () => { 107 | const id = uuidv4(); 108 | const regex = 109 | /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/; 110 | expect(regex.test(id)).equal(true); 111 | }); 112 | 113 | it('compileTemplate adds methods to element', () => { 114 | class Element {} 115 | compileTemplate({ selector: 'x-element' }, Element); 116 | const compiled: ReadymadeElement = new Element() as ReadymadeElement; 117 | expect(compiled.elementMeta?.selector).equal('x-element'); 118 | expect(compiled.elementMeta?.eventMap).does.not.equal(undefined); 119 | expect(compiled.template).does.not.equal(undefined); 120 | expect(compiled.bindTemplate).does.not.equal(undefined); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /cypress/integration/unit/core/event.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { EventDispatcher } from '../../../../src/modules/core/event'; 3 | 4 | let fromElement: HTMLElement; 5 | let toElement: HTMLElement; 6 | let uniElement: HTMLElement; 7 | let event: CustomEvent; 8 | let dispatcher: EventDispatcher; 9 | let receiver: EventDispatcher; 10 | let broadcaster: EventDispatcher; 11 | 12 | describe('EventDispatcher Test', () => { 13 | 14 | beforeEach(() => { 15 | fromElement = document.createElement('div'); 16 | toElement = document.createElement('div'); 17 | uniElement = document.createElement('div'); 18 | event = new CustomEvent('test', { detail: 'check' } ); 19 | dispatcher = new EventDispatcher(fromElement, 'channel-one'); 20 | receiver = new EventDispatcher(toElement, 'channel-one'); 21 | broadcaster = new EventDispatcher(uniElement); 22 | }); 23 | 24 | it('dispatcher has a target that equals fromElement', () => { 25 | expect(dispatcher.target).equals(fromElement); 26 | }); 27 | 28 | it('dispatcher has channel name called channel-one', () => { 29 | expect(dispatcher.channels['channel-one'].name).equals('channel-one'); 30 | }); 31 | 32 | it('dispatcher stores a CustomEvent named test', () => { 33 | dispatcher.set('test', event); 34 | expect(dispatcher.events.test).equals(event); 35 | }); 36 | 37 | it('dispatcher can add channel', () => { 38 | dispatcher.setChannel('new-channel'); 39 | expect(dispatcher.channels['new-channel'].name).equals('new-channel'); 40 | }); 41 | 42 | it('dispatcher can remove channel', () => { 43 | dispatcher.setChannel('new-channel'); 44 | dispatcher.removeChannel('new-channel'); 45 | expect(dispatcher.channels['new-channel']).equals(undefined); 46 | }); 47 | 48 | it('receiver has a target that equals toElement', () => { 49 | expect(receiver.target).equals(toElement); 50 | }); 51 | 52 | it('broadcaster has a target that equals uniElement', () => { 53 | expect(broadcaster.target).equals(uniElement); 54 | }); 55 | 56 | it('broadcaster has default channel name', () => { 57 | expect(broadcaster.channels.default.name).equals('default'); 58 | expect(broadcaster.channels['channel-one']).equals(undefined); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = () => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }); 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); 26 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | Cypress.on('uncaught:exception', (err, runnable) => { 23 | // returning false here prevents Cypress from 24 | // failing the test 25 | return false; 26 | }); -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "baseUrl": "../node_modules", 7 | "target": "es5", 8 | "lib": ["es5", "dom"], 9 | "types": ["cypress", "node"] 10 | }, 11 | "include": ["./*/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default [ 6 | { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 7 | pluginJs.configs.recommended, 8 | ...tseslint.configs.recommended, 9 | { 10 | files: ['**/*.{js,mjs,cjs,ts}'], 11 | rules: { 12 | '@typescript-eslint/no-explicit-any': 'off', 13 | 'no-loss-of-precision': 'off', 14 | 'no-prototype-builtins': 'off', 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readymade", 3 | "version": "3.1.0", 4 | "description": "JavaScript microlibrary for developing Web Components with Decorators", 5 | "type": "module", 6 | "main": "index.html", 7 | "targets": { 8 | "main": false, 9 | "node": { 10 | "context": "node", 11 | "outputFormat": "commonjs", 12 | "source": "src/server/index.ts", 13 | "distDir": "dist/server" 14 | }, 15 | "dev": { 16 | "source": "src/client/index.html", 17 | "distDir": "dist/client", 18 | "publicUrl": "/" 19 | } 20 | }, 21 | "engines": { 22 | "node": "20" 23 | }, 24 | "scripts": { 25 | "start": "cross-env NODE_ENV=development vite-node ./src/server/index.ts --config vite.config.server.js", 26 | "clean": "yarn clean:dist && yarn clean:packages", 27 | "clean:dist": "rimraf dist", 28 | "clean:packages": "rimraf packages", 29 | "deploy:gh-pages": "gh-pages --dist-dir dist/client --branch gh-pages", 30 | "start:client": "cross-env NODE_ENV=development vite dev ./src/client --port 4443 --config vite.config.js", 31 | "lint": "eslint src/**/*.ts --fix", 32 | "build": "cross-env NODE_ENV=production yarn clean:dist && yarn build:prod", 33 | "build:prod": "yarn clean:dist && concurrently \"yarn build:index\" \"yarn build:routes\" \"yarn build:server\"", 34 | "build:client": "cross-env NODE_ENV=development vite build ./src/client/ --config vite.config.inline.js --outDir ../../dist/client", 35 | "build:library": "yarn clean:dist && bash ./BUILD.sh", 36 | "build:index": "vite build ./src/client/ --outDir ../../dist/client --config vite.config.index.js", 37 | "build:routes": "vite build ./src/client/ --outDir ../../dist/client --config vite.config.routes.js", 38 | "build:server": "vite build --outDir dist/server --ssr src/server/index.ts --config vite.config.server.js", 39 | "build:hello": "vite build ./src/client/hello.ts --outDir ../../../dist --config vite.config.hello.js", 40 | "pretty": "prettier --write \"src/**/*.{js,ts,html,css,scss}\"", 41 | "pretty:check": "prettier \"src/**/*.{js,ts,html,css,scss}\" --check", 42 | "serve": "cross-env NODE_ENV=production node dist/server/index.js", 43 | "test": "cypress run", 44 | "test:chrome": "cypress run --browser chrome", 45 | "test:open": "cypress open --browser chrome" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/steveblue/custom-elements.git" 50 | }, 51 | "keywords": [ 52 | "custom", 53 | "elements", 54 | "web", 55 | "components", 56 | "custom", 57 | "elements", 58 | "api", 59 | "ui", 60 | "library" 61 | ], 62 | "author": "Stephen Belovarich", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/steveblue/custom-elements/issues" 66 | }, 67 | "homepage": "https://github.com/steveblue/custom-elements#readme", 68 | "dependencies": { 69 | "@lit-labs/ssr": "^3.2.2", 70 | "@ungap/custom-elements": "^1.3.0", 71 | "broadcastchannel-polyfill": "^1.0.1", 72 | "chalk": "^5.3.0", 73 | "cheerio": "^1.0.0", 74 | "compression": "^1.7.4", 75 | "concurrently": "^9.0.1", 76 | "cors": "^2.8.5", 77 | "cross-env": "^7.0.3", 78 | "element-internals-polyfill": "^1.3.12", 79 | "express": "^4.21.0", 80 | "gh-pages": "^6.1.1", 81 | "he": "^1.2.0", 82 | "html-minifier-terser": "^7.2.0", 83 | "http": "^0.0.0", 84 | "https": "^1.0.0", 85 | "jsdom": "^25.0.1", 86 | "lit-html": "^3.2.0", 87 | "node-fetch": "2.6.7", 88 | "node-window-polyfill": "^1.0.2", 89 | "osc": "^2.4.5", 90 | "prismjs": "^1.29.0", 91 | "rimraf": "^6.0.1", 92 | "vite-node": "^2.1.2", 93 | "ws": "^8.18.0" 94 | }, 95 | "devDependencies": { 96 | "@eslint/js": "^9.12.0", 97 | "@rollup/plugin-node-resolve": "^15.3.0", 98 | "@rollup/plugin-terser": "^0.4.4", 99 | "@rollup/plugin-typescript": "^12.1.0", 100 | "@types/gh-pages": "^6", 101 | "@types/he": "^1", 102 | "@types/node": "^22.7.4", 103 | "@types/ws": "^8", 104 | "@typescript-eslint/eslint-plugin": "^8.8.0", 105 | "@typescript-eslint/parser": "^8.8.0", 106 | "chokidar": "^4.0.1", 107 | "chromedriver": "^129.0.2", 108 | "copyfiles": "^2.4.1", 109 | "cssnano": "^7.0.6", 110 | "cypress": "^13.15.0", 111 | "cypress-ct-lit": "^0.5.0", 112 | "eslint": "^9.12.0", 113 | "eslint-config-prettier": "^9.1.0", 114 | "eslint-plugin-prettier": "^5.2.1", 115 | "glob": "^11.0.0", 116 | "globals": "^15.10.0", 117 | "helmet": "^8.0.0", 118 | "http-server": "^14.1.1", 119 | "husky": "^9.1.6", 120 | "lint-staged": "^15.2.10", 121 | "postcss": "^8.2.15", 122 | "prettier": "^3.3.3", 123 | "rollup": "4.24.0", 124 | "rollup-plugin-cleanup": "^3.2.1", 125 | "rollup-plugin-dts": "^6.1.1", 126 | "rollup-plugin-inline-postcss": "^3.0.1", 127 | "rollup-plugin-minify-html-literals": "^1.2.6", 128 | "rollup-plugin-postcss": "^4.0.2", 129 | "sass": "^1.79.4", 130 | "stream-browserify": "3", 131 | "tachometer": "^0.7.1", 132 | "tslib": "~2.7.0", 133 | "typescript": "~5.5.0", 134 | "typescript-eslint": "^8.8.1", 135 | "vite": "^5.4.8", 136 | "vite-plugin-alias": "^0.1.1", 137 | "vite-plugin-html": "^3.2.2", 138 | "vite-plugin-node-polyfills": "^0.22.0", 139 | "vite-plugin-singlefile": "^2.0.2", 140 | "vite-plugin-standard-css-modules": "^0.0.11", 141 | "vite-plugin-static-copy": "^2.0.0", 142 | "vite-plugin-tsconfig-paths": "^1.4.1", 143 | "vite-resolve-tsconfig-paths": "^0.0.8", 144 | "vite-tsconfig-paths": "^5.0.1" 145 | }, 146 | "browserslist": "> 0.5%, last 2 versions, not dead", 147 | "externals": [ 148 | "./src/server/config.js" 149 | ], 150 | "packageManager": "yarn@4.5.0" 151 | } -------------------------------------------------------------------------------- /src/client/app/component/button.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, Emitter, html, Listen, State } from '@readymade/core'; 2 | import { ButtonComponent } from '@readymade/dom'; 3 | 4 | class ButtonState { 5 | public model: string = 'Click'; 6 | } 7 | 8 | @Component({ 9 | selector: 'my-button', 10 | custom: { extends: 'button' }, 11 | style: css` 12 | :host { 13 | background: rgba(24, 24, 24, 1); 14 | cursor: pointer; 15 | color: white; 16 | font-weight: 400; 17 | } 18 | `, 19 | template: html` {{model}} `, 20 | }) 21 | class MyButtonComponent extends ButtonComponent { 22 | constructor() { 23 | super(); 24 | } 25 | 26 | @State() 27 | public getState() { 28 | return { 29 | model: 'Click', 30 | }; 31 | } 32 | 33 | @Emitter('bang', { bubbles: true, composed: true }) 34 | @Listen('click') 35 | public onClick() { 36 | this.emitter.broadcast('bang'); 37 | } 38 | @Listen('keyup') 39 | public onKeyUp(event: KeyboardEvent) { 40 | if (event.key === 'Enter') { 41 | this.emitter.broadcast('bang'); 42 | } 43 | } 44 | } 45 | 46 | export { ButtonState, MyButtonComponent }; 47 | -------------------------------------------------------------------------------- /src/client/app/component/code.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html, State } from '@readymade/core'; 2 | 3 | declare let Prism: any; 4 | 5 | export class CodeState { 6 | public type: string = ''; 7 | public language: string = ''; 8 | } 9 | 10 | @Component({ 11 | selector: 'r-code', 12 | style: css` 13 | :host { 14 | display: block; 15 | padding: 1em; 16 | background: var(--ready-color-container-bg); 17 | } 18 | code[class*='language-'], 19 | pre[class*='language-'] { 20 | -moz-tab-size: 2; 21 | -o-tab-size: 2; 22 | tab-size: 2; 23 | -webkit-hyphens: none; 24 | -moz-hyphens: none; 25 | -ms-hyphens: none; 26 | hyphens: none; 27 | white-space: pre; 28 | white-space: pre-wrap; 29 | word-wrap: normal; 30 | font-family: 'Source Code Pro', 'Courier New', monospace; 31 | font-size: 14px; 32 | font-weight: 400; 33 | color: #e0e2e4; 34 | text-shadow: none; 35 | } 36 | ::selection { 37 | background: #ff7de9; /* WebKit/Blink Browsers */ 38 | } 39 | ::-moz-selection { 40 | background: #ff7de9; /* Gecko Browsers */ 41 | } 42 | pre[class*='language-'], 43 | :not(pre) > code[class*='language-'] { 44 | background: #0e1014; 45 | } 46 | pre[class*='language-'] { 47 | padding: 15px; 48 | border-radius: 4px; 49 | border: 1px solid #0e1014; 50 | overflow: auto; 51 | } 52 | 53 | pre[class*='language-'] { 54 | position: relative; 55 | } 56 | pre[class*='language-'] code { 57 | white-space: pre; 58 | display: block; 59 | } 60 | 61 | :not(pre) > code[class*='language-'] { 62 | padding: 0.2em 0.2em; 63 | border-radius: 0.3em; 64 | border: 0.13em solid #7a6652; 65 | box-shadow: 1px 1px 0.3em -0.1em #000 inset; 66 | } 67 | .token.namespace { 68 | opacity: 0.7; 69 | } 70 | .token.function { 71 | color: rgba(117, 191, 255, 1); 72 | } 73 | .token.class-name { 74 | color: #e0e2e4; 75 | } 76 | .token.comment, 77 | .token.prolog, 78 | .token.doctype, 79 | .token.cdata { 80 | color: #208c9a; 81 | } 82 | .token.operator, 83 | .token.boolean, 84 | .token.number { 85 | color: #ff7de9; 86 | } 87 | .token.attr-name, 88 | .token.string { 89 | color: #e6d06c; 90 | } 91 | 92 | .token.entity, 93 | .token.url, 94 | .language-css .token.string, 95 | .style .token.string { 96 | color: #bb9cf1; 97 | } 98 | .token.selector, 99 | .token.inserted { 100 | color: #b6babf; 101 | } 102 | .token.atrule, 103 | .token.attr-value, 104 | .token.keyword, 105 | .token.important, 106 | .token.deleted { 107 | color: #ff7de9; 108 | } 109 | .token.regex, 110 | .token.statement { 111 | color: #ffe4a6; 112 | } 113 | .token.placeholder, 114 | .token.variable { 115 | color: #ff7de9; 116 | } 117 | .token.important, 118 | .token.statement, 119 | .token.bold { 120 | font-weight: bold; 121 | } 122 | .token.punctuation { 123 | color: #a9bacb; 124 | } 125 | .token.entity { 126 | cursor: help; 127 | } 128 | .token.italic { 129 | font-style: italic; 130 | } 131 | 132 | code.language-markup { 133 | color: #b1b1b3; 134 | } 135 | code.language-markup .token.tag { 136 | color: #75bfff; 137 | } 138 | code.language-markup .token.attr-name { 139 | color: #ff97e9; 140 | } 141 | code.language-markup .token.attr-value { 142 | color: #d7d7db; 143 | } 144 | code.language-markup .token.style, 145 | code.language-markup .token.script { 146 | color: #75bfff99; 147 | } 148 | code.language-markup .token.script .token.keyword { 149 | color: #9f9f9f; 150 | } 151 | 152 | pre[class*='language-'][data-line] { 153 | position: relative; 154 | padding: 1em 0 1em 3em; 155 | } 156 | pre[data-line] .line-highlight { 157 | position: absolute; 158 | left: 0; 159 | right: 0; 160 | padding: 0; 161 | margin-top: 1em; 162 | background: rgba(255, 255, 255, 0.08); 163 | pointer-events: none; 164 | line-height: inherit; 165 | white-space: pre; 166 | } 167 | pre[data-line] .line-highlight:before, 168 | pre[data-line] .line-highlight[data-end]:after { 169 | content: attr(data-start); 170 | position: absolute; 171 | top: 0.4em; 172 | left: 0.6em; 173 | min-width: 1em; 174 | padding: 0.2em 0.5em; 175 | background-color: rgba(255, 255, 255, 0.4); 176 | color: black; 177 | font: bold 65%/1 sans-serif; 178 | height: 1em; 179 | line-height: 1em; 180 | text-align: center; 181 | border-radius: 999px; 182 | text-shadow: none; 183 | box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7); 184 | } 185 | pre[data-line] .line-highlight[data-end]:after { 186 | content: attr(data-end); 187 | top: auto; 188 | bottom: 0.4em; 189 | } 190 | `, 191 | template: html` 192 |
193 | 194 | `, 195 | }) 196 | class RCodeComponent extends CustomElement { 197 | constructor() { 198 | super(); 199 | } 200 | 201 | connectedCallback() { 202 | this.onSlotChange(); 203 | } 204 | 205 | @State() 206 | public getState() { 207 | return new CodeState(); 208 | } 209 | 210 | static get observedAttributes() { 211 | return ['language']; 212 | } 213 | 214 | public attributeChangedCallback(name, oldValue, newValue) { 215 | switch (name) { 216 | case 'language': 217 | this.setState('type', newValue); 218 | this.setState('language', `language-${newValue}`); 219 | break; 220 | } 221 | } 222 | 223 | public onSlotChange() { 224 | const code = ( 225 | this.shadowRoot.querySelector('slot').assignedNodes() as any 226 | )[1].textContent; 227 | this.shadowRoot.querySelector('code').innerHTML = Prism.highlight( 228 | code, 229 | Prism.languages[this.getAttribute('type')], 230 | this.getAttribute('type'), 231 | ); 232 | } 233 | } 234 | 235 | export { RCodeComponent }; 236 | -------------------------------------------------------------------------------- /src/client/app/component/counter.ts: -------------------------------------------------------------------------------- 1 | import { Component, State, CustomElement } from '@readymade/core'; 2 | 3 | const CounterState = { 4 | count: 0, 5 | }; 6 | 7 | @Component({ 8 | selector: 'my-counter', 9 | template: ` 10 | 11 | {{ count }} 12 | 13 | `, 14 | style: ` 15 | span, 16 | button { 17 | font-size: 200%; 18 | } 19 | 20 | span { 21 | width: 4rem; 22 | display: inline-block; 23 | text-align: center; 24 | } 25 | 26 | button { 27 | width: 4rem; 28 | height: 4rem; 29 | border: none; 30 | border-radius: 10px; 31 | background-color: seagreen; 32 | color: white; 33 | } 34 | `, 35 | }) 36 | export class MyCounter extends CustomElement { 37 | connectedCallback() { 38 | this.shadowRoot 39 | .querySelector('#inc') 40 | .addEventListener('click', this.inc.bind(this)); 41 | this.shadowRoot 42 | .querySelector('#dec') 43 | .addEventListener('click', this.dec.bind(this)); 44 | } 45 | 46 | @State() 47 | public getState() { 48 | return CounterState; 49 | } 50 | 51 | inc() { 52 | this.setState('count', this.getState().count + 1); 53 | } 54 | 55 | dec() { 56 | this.setState('count', this.getState().count - 1); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/client/app/component/grid.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html, State } from '@readymade/core'; 2 | 3 | export class GridState { 4 | public grid: string[] = []; 5 | } 6 | 7 | export const _gridState = new GridState(); 8 | 9 | @Component({ 10 | selector: 'r-grid', 11 | style: css` 12 | :host { 13 | display: grid; 14 | grid-template-columns: repeat(16, 20px); 15 | grid-template-rows: auto; 16 | column-gap: 10px; 17 | row-gap: 10px; 18 | } 19 | `, 20 | template: html` 21 |
{{grid[0]}}
22 |
{{grid[1]}}
23 |
{{grid[2]}}
24 |
{{grid[3]}}
25 |
{{grid[4]}}
26 |
{{grid[5]}}
27 |
{{grid[6]}}
28 |
{{grid[7]}}
29 |
{{grid[8]}}
30 |
{{grid[9]}}
31 |
{{grid[10]}}
32 |
{{grid[11]}}
33 |
{{grid[12]}}
34 |
{{grid[13]}}
35 |
{{grid[14]}}
36 |
{{grid[15]}}
37 |
{{grid[16]}}
38 |
{{grid[17]}}
39 |
{{grid[18]}}
40 |
{{grid[19]}}
41 |
{{grid[20]}}
42 |
{{grid[21]}}
43 |
{{grid[22]}}
44 |
{{grid[23]}}
45 |
{{grid[24]}}
46 |
{{grid[25]}}
47 |
{{grid[26]}}
48 |
{{grid[27]}}
49 |
{{grid[28]}}
50 |
{{grid[29]}}
51 |
{{grid[30]}}
52 |
{{grid[31]}}
53 |
{{grid[32]}}
54 |
{{grid[33]}}
55 |
{{grid[34]}}
56 |
{{grid[35]}}
57 |
{{grid[36]}}
58 |
{{grid[37]}}
59 |
{{grid[38]}}
60 |
{{grid[39]}}
61 |
{{grid[40]}}
62 |
{{grid[41]}}
63 |
{{grid[42]}}
64 |
{{grid[43]}}
65 |
{{grid[44]}}
66 |
{{grid[45]}}
67 |
{{grid[46]}}
68 |
{{grid[47]}}
69 |
{{grid[48]}}
70 |
{{grid[49]}}
71 |
{{grid[50]}}
72 |
{{grid[51]}}
73 |
{{grid[52]}}
74 |
{{grid[53]}}
75 |
{{grid[54]}}
76 |
{{grid[55]}}
77 |
{{grid[56]}}
78 |
{{grid[57]}}
79 |
{{grid[58]}}
80 |
{{grid[59]}}
81 |
{{grid[60]}}
82 |
{{grid[61]}}
83 |
{{grid[62]}}
84 |
{{grid[63]}}
85 |
{{grid[64]}}
86 |
{{grid[65]}}
87 |
{{grid[66]}}
88 |
{{grid[67]}}
89 |
{{grid[68]}}
90 |
{{grid[69]}}
91 |
{{grid[70]}}
92 |
{{grid[71]}}
93 |
{{grid[72]}}
94 |
{{grid[73]}}
95 |
{{grid[74]}}
96 |
{{grid[75]}}
97 |
{{grid[76]}}
98 |
{{grid[77]}}
99 |
{{grid[78]}}
100 |
{{grid[79]}}
101 |
{{grid[80]}}
102 |
{{grid[81]}}
103 |
{{grid[82]}}
104 |
{{grid[83]}}
105 |
{{grid[84]}}
106 |
{{grid[85]}}
107 |
{{grid[86]}}
108 |
{{grid[87]}}
109 |
{{grid[88]}}
110 |
{{grid[89]}}
111 |
{{grid[90]}}
112 |
{{grid[91]}}
113 |
{{grid[92]}}
114 |
{{grid[93]}}
115 |
{{grid[94]}}
116 |
{{grid[95]}}
117 |
{{grid[96]}}
118 |
{{grid[97]}}
119 |
{{grid[98]}}
120 |
{{grid[99]}}
121 |
{{grid[100]}}
122 |
{{grid[101]}}
123 |
{{grid[102]}}
124 |
{{grid[103]}}
125 |
{{grid[104]}}
126 |
{{grid[105]}}
127 |
{{grid[106]}}
128 |
{{grid[107]}}
129 |
{{grid[108]}}
130 |
{{grid[109]}}
131 |
{{grid[110]}}
132 |
{{grid[111]}}
133 |
{{grid[112]}}
134 |
{{grid[113]}}
135 |
{{grid[114]}}
136 |
{{grid[115]}}
137 |
{{grid[116]}}
138 |
{{grid[117]}}
139 |
{{grid[118]}}
140 |
{{grid[119]}}
141 |
{{grid[120]}}
142 |
{{grid[121]}}
143 |
{{grid[122]}}
144 |
{{grid[123]}}
145 |
{{grid[124]}}
146 |
{{grid[125]}}
147 |
{{grid[126]}}
148 |
{{grid[127]}}
149 | `, 150 | }) 151 | class RGridComponent extends CustomElement { 152 | constructor() { 153 | super(); 154 | } 155 | 156 | @State() 157 | public getState() { 158 | return _gridState; 159 | } 160 | 161 | public refreshGrid() { 162 | const grid = []; 163 | for (let i = 0; i < 128; i++) { 164 | grid[i] = Math.floor(Math.random() * 128) + 1; 165 | } 166 | this.setState('grid', grid); 167 | } 168 | 169 | public animateGrid() { 170 | this.refreshGrid(); 171 | window.requestAnimationFrame(this.animateGrid.bind(this)); 172 | } 173 | 174 | public connectedCallback() { 175 | this.animateGrid(); 176 | } 177 | } 178 | 179 | export { RGridComponent }; 180 | -------------------------------------------------------------------------------- /src/client/app/component/headline.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement, State } from '@readymade/core'; 2 | 3 | const style = ` 4 | :host { 5 | font-size: 16px; 6 | } 7 | h1 { 8 | font-family: 'Major Mono Display', sans-serif; 9 | padding-left: 1em; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-margin-before: 0px; 13 | -webkit-margin-after: 0px; 14 | } 15 | h1, 16 | span { 17 | font-size: 1em; 18 | letter-spacing: -0.04em; 19 | margin-block-start: 0em; 20 | margin-block-end: 0em; 21 | } 22 | h1.is--default, 23 | span.is--default { 24 | font-size: 1em; 25 | } 26 | h1.is--small, 27 | span.is--small { 28 | font-size: 12px; 29 | } 30 | h1.is--medium, 31 | span.is--medium { 32 | font-size: 6em; 33 | } 34 | h1.is--large, 35 | span.is--large { 36 | font-size: 12em; 37 | padding-left: 0em; 38 | } 39 | `; 40 | 41 | const template = `

{{ model.copy }}

`; 42 | 43 | @Component({ 44 | selector: 'r-headline', 45 | style, 46 | template, 47 | }) 48 | class RHeadlineComponent extends CustomElement { 49 | public hyperNode: any; 50 | public model: { 51 | copy?: string | number; 52 | size?: string; 53 | }; 54 | 55 | constructor() { 56 | super(); 57 | } 58 | 59 | @State() 60 | public getState() { 61 | return { 62 | model: { 63 | size: '', 64 | copy: '', 65 | }, 66 | }; 67 | } 68 | 69 | static get observedAttributes() { 70 | return ['headline', 'size']; 71 | } 72 | public attributeChangedCallback(name, oldValue, newValue) { 73 | switch (name) { 74 | case 'headline': 75 | this.setState('model.copy', newValue); 76 | break; 77 | case 'size': 78 | this.setState('model.size', newValue); 79 | break; 80 | } 81 | } 82 | } 83 | 84 | const render = ({ size, copy }) => ` 85 | 86 | 92 | 93 | `; 94 | 95 | export { RHeadlineComponent, render }; 96 | -------------------------------------------------------------------------------- /src/client/app/component/hello.state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | CustomElement, 4 | html, 5 | State, 6 | Emitter, 7 | Listen, 8 | } from '@readymade/core'; 9 | 10 | class HelloState { 11 | public model: string = 'Hello World'; 12 | } 13 | 14 | @Component({ 15 | selector: 'hello-state', 16 | template: html` {{model}} `, 17 | }) 18 | class HelloStateComponent extends CustomElement { 19 | constructor() { 20 | super(); 21 | } 22 | 23 | @State() 24 | public getState() { 25 | return new HelloState(); 26 | } 27 | @Emitter('bang') 28 | @Listen('click') 29 | public onClick() { 30 | this.emitter.broadcast('bang'); 31 | } 32 | @Listen('keyup') 33 | public onKeyUp(event) { 34 | if (event.key === 'Enter') { 35 | this.emitter.broadcast('bang'); 36 | } 37 | } 38 | } 39 | 40 | export { HelloState, HelloStateComponent }; 41 | -------------------------------------------------------------------------------- /src/client/app/component/hello.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'hello-world', 5 | template: ` Hello World `, 6 | }) 7 | export class HelloComponent extends CustomElement {} 8 | -------------------------------------------------------------------------------- /src/client/app/component/input.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, Listen } from '@readymade/core'; 2 | import { InputComponent } from '@readymade/dom'; 3 | 4 | @Component({ 5 | selector: 'my-input', 6 | custom: { extends: 'input' }, 7 | style: css` 8 | :host { 9 | background: rgba(24, 24, 24, 1); 10 | border: 0px none; 11 | color: white; 12 | } 13 | `, 14 | }) 15 | class MyInputComponent extends InputComponent { 16 | constructor() { 17 | super(); 18 | } 19 | @Listen('focus') 20 | public onFocus() { 21 | this.value = 'input'; 22 | } 23 | } 24 | 25 | export { MyInputComponent }; 26 | -------------------------------------------------------------------------------- /src/client/app/component/item.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html, Listen } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'my-item', 5 | style: css` 6 | :host { 7 | display: block; 8 | cursor: pointer; 9 | } 10 | :host([state='--selected']) { 11 | background: rgba(255, 105, 180, 1); 12 | color: black; 13 | font-weight: 700; 14 | } 15 | `, 16 | template: html` 17 |

18 | item 19 |

20 | `, 21 | }) 22 | class MyItemComponent extends CustomElement { 23 | constructor() { 24 | super(); 25 | } 26 | @Listen('bang', 'default') 27 | public onBang() { 28 | if (this.getAttribute('state') === '--selected') { 29 | this.setAttribute('state', ''); 30 | } else { 31 | this.setAttribute('state', '--selected'); 32 | } 33 | } 34 | } 35 | 36 | export { MyItemComponent }; 37 | -------------------------------------------------------------------------------- /src/client/app/component/list.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | css, 4 | CustomElement, 5 | getElementIndex, 6 | getSiblings, 7 | html, 8 | Listen, 9 | } from '@readymade/core'; 10 | 11 | @Component({ 12 | selector: 'my-list', 13 | style: css` 14 | :host { 15 | display: block; 16 | background: rgba(24, 24, 24, 1); 17 | width: 200px; 18 | height: 200px; 19 | color: white; 20 | padding: 1em; 21 | border-radius: 8px; 22 | } 23 | `, 24 | template: html` `, 25 | }) 26 | class MyListComponent extends CustomElement { 27 | public currentIndex: number; 28 | constructor() { 29 | super(); 30 | this.currentIndex = 0; 31 | } 32 | public deactivateElement(elem: Element) { 33 | elem.setAttribute('tabindex', '-1'); 34 | elem.querySelector('my-item').setAttribute('state', ''); 35 | } 36 | public activateElement(elem: Element) { 37 | elem.setAttribute('tabindex', '0'); 38 | elem.querySelector('my-item').setAttribute('state', '--selected'); 39 | } 40 | public connectedCallback() { 41 | this.setAttribute('tabindex', '0'); 42 | } 43 | @Listen('focus') 44 | public onFocus() { 45 | for (const li of this.children[0].children) { 46 | if (li === this.children[0].children[this.currentIndex]) { 47 | this.activateElement(li); 48 | } else { 49 | this.deactivateElement(li); 50 | } 51 | li.addEventListener('click', () => { 52 | getSiblings(li).forEach((elem: Element) => { 53 | this.deactivateElement(elem); 54 | }); 55 | this.activateElement(li); 56 | this.onSubmit(); 57 | }); 58 | } 59 | } 60 | @Listen('keydown') 61 | public onKeydown(ev: KeyboardEvent) { 62 | const currentElement = this.querySelector( 63 | '[tabindex]:not([tabindex="-1"])', 64 | ); 65 | const siblings = getSiblings(currentElement); 66 | this.currentIndex = getElementIndex(currentElement); 67 | if (ev.keyCode === 13) { 68 | this.onSubmit(); 69 | } 70 | if (ev.keyCode === 38) { 71 | // up 72 | if (this.currentIndex === 0) { 73 | this.currentIndex = siblings.length - 1; 74 | } else { 75 | this.currentIndex -= 1; 76 | } 77 | siblings.forEach((elem: Element) => { 78 | if (getElementIndex(elem) === this.currentIndex) { 79 | this.activateElement(elem); 80 | } else { 81 | this.deactivateElement(elem); 82 | } 83 | }); 84 | } 85 | if (ev.keyCode === 40) { 86 | // down 87 | if (this.currentIndex === siblings.length - 1) { 88 | this.currentIndex = 0; 89 | } else { 90 | this.currentIndex += 1; 91 | } 92 | siblings.forEach((elem: Element) => { 93 | if (getElementIndex(elem) === this.currentIndex) { 94 | this.activateElement(elem); 95 | } else { 96 | this.deactivateElement(elem); 97 | } 98 | }); 99 | } 100 | } 101 | public onSubmit() { 102 | // noop? 103 | } 104 | } 105 | 106 | export { MyListComponent }; 107 | -------------------------------------------------------------------------------- /src/client/app/component/logo.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement, State } from '@readymade/core'; 2 | import { render as renderHeadline } from './headline'; 3 | 4 | export class LogoState { 5 | public heading: string = 'R'; 6 | public heading2: string = 'readymade'; 7 | public size: string = ''; 8 | public sizes: string[] = ['is--small', 'is--medium', 'is--large']; 9 | } 10 | 11 | export const _logoState = new LogoState(); 12 | 13 | const style = ` 14 | :host { 15 | display: block; 16 | user-select: none; 17 | font-size: 16px; 18 | font-family: Source Sans Pro, sans-serif; 19 | } 20 | `; 21 | 22 | const template = ` 23 | 24 | 25 | `; 26 | 27 | @Component({ 28 | selector: 'r-logo', 29 | style, 30 | template, 31 | }) 32 | class RLogoComponent extends CustomElement { 33 | public letters: string[]; 34 | constructor() { 35 | super(); 36 | } 37 | 38 | @State() 39 | public getState() { 40 | return _logoState; 41 | } 42 | 43 | static get observedAttributes() { 44 | return ['size']; 45 | } 46 | 47 | public attributeChangedCallback(name, oldValue, newValue) { 48 | switch (name) { 49 | case 'size': 50 | this.setSize(newValue); 51 | break; 52 | } 53 | } 54 | 55 | public setSize(size: string) { 56 | this.setState('size', size); 57 | } 58 | } 59 | 60 | const render = ({ size, classes }: { size: string; classes?: string }) => ` 61 | 62 | 69 | 70 | `; 71 | 72 | export { RLogoComponent, render }; 73 | -------------------------------------------------------------------------------- /src/client/app/component/meter.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement, css, html } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'r-meter', 5 | style: css` 6 | :host { 7 | display: block; 8 | width: 100%; 9 | margin-bottom: 4px; 10 | } 11 | label { 12 | display: block; 13 | font-size: 1em; 14 | margin-bottom: 4px; 15 | } 16 | .label { 17 | margin-right: 4px; 18 | opacity: 0.8; 19 | font-weight: 500; 20 | } 21 | .meter { 22 | display: block; 23 | width: 100%; 24 | height: 20px; 25 | overflow: hidden; 26 | } 27 | .progress { 28 | display: inline-block; 29 | width: 0%; 30 | height: 100%; 31 | border-radius: 4px; 32 | transition: width 2s ease-out; 33 | } 34 | `, 35 | template: html` 36 | 37 |
38 |
39 |
40 | `, 41 | }) 42 | class RMeterComponent extends CustomElement { 43 | min: number; 44 | max: number; 45 | value: number; 46 | 47 | constructor() { 48 | super(); 49 | } 50 | 51 | static get observedAttributes() { 52 | return ['label', 'max', 'value', 'color']; 53 | } 54 | 55 | public attributeChangedCallback(name, old, next) { 56 | switch (name) { 57 | case 'label': 58 | this.setLabel(next); 59 | break; 60 | case 'max': 61 | this.max = parseFloat(next); 62 | this.setValue(); 63 | break; 64 | case 'value': 65 | this.value = parseFloat(next); 66 | this.setValue(); 67 | break; 68 | case 'color': 69 | this.setColor(next); 70 | break; 71 | } 72 | } 73 | 74 | canSet() { 75 | if (this.max === undefined || this.value === undefined) { 76 | return false; 77 | } 78 | return true; 79 | } 80 | 81 | setValue() { 82 | if (this.canSet()) { 83 | ( 84 | (this.shadowRoot.querySelector('.progress')) as HTMLElement 85 | ).style.width = `${(this.value / this.max) * 100}%`; 86 | ( 87 | (this.shadowRoot.querySelector('.value')) as HTMLElement 88 | ).innerText = `${this.value}Kb`; 89 | } 90 | } 91 | 92 | setLabel(val: string) { 93 | ( 94 | (this.shadowRoot.querySelector('.label')) as HTMLElement 95 | ).innerText = val; 96 | } 97 | 98 | setColor(val: string) { 99 | ( 100 | (this.shadowRoot.querySelector('.progress')) as HTMLElement 101 | ).style.background = val; 102 | } 103 | } 104 | 105 | export { RMeterComponent }; 106 | -------------------------------------------------------------------------------- /src/client/app/component/stats.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html } from '@readymade/core'; 2 | 3 | const env = process.env.NODE_ENV || 'development'; 4 | 5 | @Component({ 6 | selector: 'r-stats', 7 | style: css` 8 | :host { 9 | display: block; 10 | } 11 | ::slotted(ul) { 12 | display: inline-block; 13 | position: relative; 14 | left: 50%; 15 | transform: translateX(-50%); 16 | font-weight: 300; 17 | } 18 | `, 19 | template: html` `, 20 | }) 21 | class RStatsComponent extends CustomElement { 22 | constructor() { 23 | super(); 24 | this.shadowRoot 25 | ?.querySelector('slot') 26 | ?.addEventListener('slotchange', () => this.onSlotChange()); 27 | } 28 | public onSlotChange() { 29 | this.animateIn(); 30 | } 31 | public animateIn() { 32 | const ul = this.shadowRoot?.querySelector('slot')?.assignedNodes()[ 33 | env === 'production' ? 0 : 1 34 | ]; 35 | if (ul && (ul as Element).children) { 36 | Array.from((ul as Element).children).forEach((li: Element, index) => { 37 | li.animate( 38 | [ 39 | { opacity: '0', color: '#000' }, 40 | { opacity: '0', offset: index * 0.1 }, 41 | { opacity: '1', color: '#fff' }, 42 | ], 43 | { 44 | duration: 2000, 45 | }, 46 | ); 47 | }); 48 | } 49 | } 50 | } 51 | 52 | export { RStatsComponent }; 53 | -------------------------------------------------------------------------------- /src/client/app/component/tree/atom.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html, State } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'x-atom', 5 | style: css` 6 | :host { 7 | display: flex; 8 | } 9 | `, 10 | template: html` {{astate}} `, 11 | }) 12 | class AtomComponent extends CustomElement { 13 | constructor() { 14 | super(); 15 | } 16 | 17 | @State() 18 | public getState() { 19 | return { 20 | astate: '', 21 | }; 22 | } 23 | 24 | static get observedAttributes() { 25 | return ['model']; 26 | } 27 | 28 | public attributeChangedCallback(name, oldValue, newValue) { 29 | switch (name) { 30 | case 'model': 31 | this.setModel(newValue); 32 | break; 33 | } 34 | } 35 | public setModel(model: string) { 36 | this.setState('astate', model); 37 | } 38 | } 39 | 40 | export { AtomComponent }; 41 | -------------------------------------------------------------------------------- /src/client/app/component/tree/node.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, html, CustomElement, State } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'x-node', 5 | style: css` 6 | :host { 7 | display: flex; 8 | } 9 | `, 10 | template: html` `, 11 | }) 12 | class NodeComponent extends CustomElement { 13 | constructor() { 14 | super(); 15 | } 16 | 17 | @State() 18 | public getState() { 19 | return { 20 | xnode: '', 21 | }; 22 | } 23 | 24 | static get observedAttributes() { 25 | return ['model']; 26 | } 27 | 28 | public attributeChangedCallback(name, oldValue, newValue) { 29 | switch (name) { 30 | case 'model': 31 | this.setModel(newValue); 32 | break; 33 | } 34 | } 35 | public setModel(model: string) { 36 | this.setState('xnode', model); 37 | } 38 | } 39 | 40 | export { NodeComponent }; 41 | -------------------------------------------------------------------------------- /src/client/app/component/tree/tree.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html, State } from '@readymade/core'; 2 | 3 | export class TreeState { 4 | public arrayModel = [ 5 | 'aaa', 6 | 'Node 1', 7 | 'Node 2', 8 | 'Node 3', 9 | 'Node 4', 10 | 'Node 5', 11 | 'Node 6', 12 | 'Node 7', 13 | ['far', 'fiz', 'faz', 'fuz'], 14 | ]; 15 | public objectModel = { 16 | foo: { 17 | bar: { 18 | baz: 'bbb', 19 | }, 20 | far: { 21 | fiz: { 22 | faz: { 23 | fuz: 'fuz', 24 | }, 25 | }, 26 | }, 27 | mar: { 28 | maz: 'mmm', 29 | }, 30 | }, 31 | }; 32 | public ax = 'aaa'; 33 | public bx = 'bbb'; 34 | public cx = 'ccc'; 35 | public dx = 'ddd'; 36 | public ex = 'eee'; 37 | public fx = 'fff'; 38 | public gx = 'ggg'; 39 | public hx = 'hhh'; 40 | public state: { 41 | foo: { 42 | bar: 'x'; 43 | }; 44 | }; 45 | } 46 | 47 | @Component({ 48 | selector: 'x-tree', 49 | autoDefine: false, 50 | style: css` 51 | :host { 52 | display: grid; 53 | font-size: 2em; 54 | } 55 | `, 56 | template: html` 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | `, 68 | }) 69 | class TreeComponent extends CustomElement { 70 | constructor() { 71 | super(); 72 | } 73 | 74 | @State() 75 | public getState() { 76 | return { 77 | arrayModel: [ 78 | 'aaa', 79 | 'Node 1', 80 | 'Node 2', 81 | 'Node 3', 82 | 'Node 4', 83 | 'Node 5', 84 | 'Node 6', 85 | 'Node 7', 86 | ['far', 'fiz', 'faz', 'fuz'], 87 | ], 88 | objectModel: { 89 | foo: { 90 | bar: { 91 | baz: 'bbb', 92 | }, 93 | far: { 94 | fiz: { 95 | faz: { 96 | fuz: 'fuz', 97 | }, 98 | }, 99 | }, 100 | mar: { 101 | maz: 'mmm', 102 | }, 103 | }, 104 | }, 105 | ax: 'aaa', 106 | bx: 'bbb', 107 | cx: 'ccc', 108 | dx: 'ddd', 109 | ex: 'eee', 110 | fx: 'fff', 111 | gx: 'ggg', 112 | hx: 'hhh', 113 | state: { 114 | foo: { 115 | bar: 'x', 116 | }, 117 | }, 118 | }; 119 | } 120 | 121 | static get observedAttributes() { 122 | return ['model']; 123 | } 124 | 125 | public attributeChangedCallback(name, oldValue, newValue) { 126 | switch (name) { 127 | case 'model': 128 | this.setModel(newValue); 129 | break; 130 | } 131 | } 132 | public setModel(model: string) { 133 | this.setState('state.foo.bar', model); 134 | } 135 | } 136 | 137 | customElements.define('x-tree', TreeComponent); 138 | 139 | export { TreeComponent }; 140 | -------------------------------------------------------------------------------- /src/client/app/index.ts: -------------------------------------------------------------------------------- 1 | // test components 2 | export { MyButtonComponent } from './component/button'; 3 | export { RCodeComponent } from './component/code'; 4 | export { MyCounter } from './component/counter'; 5 | export { RHeadlineComponent } from './component/headline'; 6 | export { MyInputComponent } from './component/input'; 7 | export { MyItemComponent } from './component/item'; 8 | export { MyListComponent } from './component/list'; 9 | export { RLogoComponent } from './component/logo'; 10 | export { RMainNavComponent } from './component/main-nav'; 11 | // docs 12 | export { RMeterComponent } from './component/meter'; 13 | export { RSideNavComponent } from './component/side-nav'; 14 | export { RStatsComponent } from './component/stats'; 15 | export { AtomComponent } from './component/tree/atom'; 16 | export { NodeComponent } from './component/tree/node'; 17 | export { TreeComponent } from './component/tree/tree'; 18 | // ui library 19 | export { 20 | RdButton, 21 | RdRadioGroup, 22 | RdCheckBox, 23 | RdInput, 24 | RdTextArea, 25 | RdSlider, 26 | RdSwitch, 27 | RdDropdown, 28 | RdButtonPad, 29 | } from '@readymade/ui'; 30 | // views 31 | export { HomeComponent } from './view/home'; 32 | export { PerformanceTestComponent } from './view/perf'; 33 | export { QueryComponent } from './view/query'; 34 | export { TestBedComponent } from './view/test'; 35 | export { LibraryComponent } from './view/lib'; 36 | export { NotFoundComponent } from './view/404'; 37 | export { Router, routing } from './routing'; 38 | -------------------------------------------------------------------------------- /src/client/app/routing.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@readymade/router'; 2 | 3 | const routing = [ 4 | { path: '/', component: 'app-home', title: 'Readymade' }, 5 | { path: '/test', component: 'app-testbed', title: 'Readymade Test Page' }, 6 | { path: '/lib', component: 'app-library', title: 'Readymade UI' }, 7 | { 8 | path: '/perf', 9 | component: 'app-perftest', 10 | title: 'Readymade Performance Test', 11 | }, 12 | { 13 | path: '/router', 14 | component: 'app-query', 15 | queryParams: { 16 | contentType: 'post', 17 | page: '1', 18 | header: '1', 19 | }, 20 | title: 'Readymade Router Test', 21 | }, 22 | { 23 | path: '/404', 24 | component: 'app-notfound', 25 | title: 'File Not Found', 26 | }, 27 | ]; 28 | 29 | export { Router, routing }; 30 | -------------------------------------------------------------------------------- /src/client/app/view/404/404.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | background: #000000; 4 | color: #cfcfcf; 5 | font-family: 'Source Sans Pro', sans-serif; 6 | font-weight: 400; 7 | font-size: 124px; 8 | padding: 0px; 9 | margin: 0px; 10 | width: 100%; 11 | height: 100%; 12 | min-height: 100vh; 13 | overflow-y: auto; 14 | -webkit-font-smoothing: auto; 15 | -moz-osx-font-smoothing: grayscale; 16 | text-align: center; 17 | } 18 | 19 | a:link, 20 | a:visited, 21 | a:active { 22 | color: #cfcfcf; 23 | text-decoration: none; 24 | } 25 | 26 | .app__logo { 27 | width: 256px; 28 | height: 256px; 29 | position: absolute; 30 | top: 50%; 31 | left: 50%; 32 | transform: translateX(-50%) translateY(-50%); 33 | perspective: 1000px; 34 | } 35 | 36 | .app__icon { 37 | display: block; 38 | width: 100%; 39 | height: 100%; 40 | opacity: 0; 41 | transform: translateZ(-1000px); 42 | & img { 43 | width: 100%; 44 | height: 100%; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/app/view/404/404.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/client/app/view/404/404.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement } from '@readymade/core'; 2 | import template from './404.html?raw'; 3 | import style from './404.css?raw'; 4 | 5 | @Component({ 6 | selector: 'app-notfound', 7 | style, 8 | template, 9 | }) 10 | class NotFoundComponent extends CustomElement { 11 | constructor() { 12 | super(); 13 | } 14 | public connectedCallback() { 15 | this.animateIn(); 16 | } 17 | public animateIn() { 18 | if (!this.shadowRoot.querySelector) return; 19 | this.shadowRoot.querySelector('.app__icon').animate( 20 | [ 21 | { opacity: '0', transform: 'translateZ(-1000px)' }, 22 | { opacity: '1', transform: 'translateZ(0px)' }, 23 | ], 24 | { 25 | duration: 2000, 26 | easing: 'cubic-bezier(0.19, 1, 0.22, 1)', 27 | fill: 'forwards', 28 | }, 29 | ); 30 | } 31 | } 32 | 33 | const render = () => ` 34 | 35 | 41 | 42 | `; 43 | 44 | export { NotFoundComponent, render }; 45 | -------------------------------------------------------------------------------- /src/client/app/view/404/index.ts: -------------------------------------------------------------------------------- 1 | export { NotFoundComponent, render } from './404'; 2 | -------------------------------------------------------------------------------- /src/client/app/view/home/home.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background: #ff7de9; /* WebKit/Blink Browsers */ 3 | } 4 | 5 | ::-moz-selection { 6 | background: #ff7de9; /* Gecko Browsers */ 7 | } 8 | 9 | button, 10 | input { 11 | color: white; 12 | font-size: 0.8em; 13 | padding: 10px; 14 | box-sizing: border-box; 15 | text-decoration: none; 16 | outline: none; 17 | box-shadow: 0px 0px 0px transparent; 18 | border: 1px solid transparent; 19 | border-radius: 4px; 20 | transition-property: box-shadow, border; 21 | transition-duration: 300ms; 22 | transition-timing-function: ease-in-out; 23 | } 24 | 25 | ul { 26 | padding: 0; 27 | margin: 0; 28 | list-style: none; 29 | -webkit-margin-start: 0px; 30 | -webkit-margin-end: 0px; 31 | -webkit-padding-start: 0px; 32 | -webkit-margin-before: 0px; 33 | -webkit-margin-after: 0px; 34 | } 35 | 36 | ul li { 37 | margin-left: 10px; 38 | margin-right: 10px; 39 | } 40 | 41 | [tabindex] { 42 | outline: 1px solid transparent; 43 | transition-property: box-shadow, border; 44 | transition-duration: 300ms; 45 | transition-timing-function: ease-in-out; 46 | } 47 | 48 | button, 49 | input { 50 | border-radius: 4px; 51 | outline: none; 52 | box-shadow: 0px 0px 0px transparent; 53 | border: 1px solid transparent; 54 | } 55 | 56 | *:focus, 57 | button:focus, 58 | input:focus { 59 | box-shadow: 0px 0px 0px rgba(255, 105, 180, 1); 60 | outline: 1px solid rgba(255, 105, 180, 1); 61 | } 62 | 63 | [hidden] { 64 | display: none !important; 65 | } 66 | 67 | a:link, 68 | a:visited { 69 | color: #cdcdcd; 70 | } 71 | 72 | a:link:hover, 73 | a:visited:hover { 74 | color: #ffffff; 75 | } 76 | 77 | h1 { 78 | font-family: 'Major Mono Display', serif; 79 | line-height: 1.5em; 80 | } 81 | 82 | h2, 83 | h3, 84 | h4, 85 | h5, 86 | h6 { 87 | font-family: 'Source Sans Pro', serif; 88 | font-weight: 400; 89 | line-height: 1.5em; 90 | } 91 | 92 | h1 { 93 | font-weight: 700; 94 | } 95 | 96 | h2 { 97 | margin-top: 2em; 98 | } 99 | 100 | h6 { 101 | font-size: 1em; 102 | } 103 | 104 | p { 105 | font-family: 'Source Sans Pro', serif; 106 | font-size: 1em; 107 | line-height: 1.5em; 108 | } 109 | 110 | .hint { 111 | opacity: 0.6; 112 | } 113 | 114 | header, 115 | section, 116 | footer { 117 | position: relative; 118 | left: 50%; 119 | max-width: 640px; 120 | transform: translateX(-50%); 121 | padding-left: 20px; 122 | padding-right: 20px; 123 | } 124 | 125 | header { 126 | padding-top: 4em; 127 | text-align: center; 128 | padding-bottom: 2em; 129 | } 130 | 131 | header h2 { 132 | font-size: 20px; 133 | font-weight: 300; 134 | margin-top: 2em; 135 | margin-bottom: 2em; 136 | } 137 | 138 | section { 139 | margin-bottom: 20px; 140 | } 141 | 142 | section h1 { 143 | padding-top: 3em; 144 | margin-bottom: 1em; 145 | font-family: 'Source Sans Pro'; 146 | } 147 | 148 | section h2 { 149 | padding: 2px 8px; 150 | background: #cfcfcf; 151 | color: #000000; 152 | font-size: 1.1em; 153 | font-weight: 400; 154 | display: inline-block; 155 | } 156 | 157 | section ul li { 158 | margin-bottom: 0.25em; 159 | } 160 | 161 | .definition__list li { 162 | padding-bottom: 0.5em; 163 | } 164 | 165 | .definition__title { 166 | font-style: italic; 167 | color: #ababab; 168 | margin-right: 0.2em; 169 | } 170 | 171 | .i__e { 172 | color: rgba(117, 191, 255, 1); 173 | } 174 | 175 | .i__c { 176 | color: #e6d06c; 177 | } 178 | 179 | footer { 180 | text-align: center; 181 | margin-top: 60px; 182 | margin-bottom: 60px; 183 | } 184 | 185 | footer p { 186 | font-family: 'Major Mono Display', sans-sarif; 187 | font-size: 0.8em; 188 | } 189 | 190 | footer r-logo { 191 | padding-bottom: 4em; 192 | } 193 | 194 | [is='my-button'] { 195 | background: #181818; 196 | cursor: pointer; 197 | color: #fff; 198 | font-weight: 400; 199 | } 200 | [is='my-input'] { 201 | background: #181818; 202 | border: 0; 203 | color: #fff; 204 | } 205 | -------------------------------------------------------------------------------- /src/client/app/view/home/index.ts: -------------------------------------------------------------------------------- 1 | export { ButtonState, MyButtonComponent } from './../../component/button'; 2 | export { RCodeComponent } from './../../component/code'; 3 | export { MyCounter } from './../../component/counter'; 4 | export { RHeadlineComponent } from './../../component/headline'; 5 | export { MyInputComponent } from './../../component/input'; 6 | export { MyItemComponent } from './../../component/item'; 7 | export { MyListComponent } from './../../component/list'; 8 | export { RLogoComponent } from './../../component/logo'; 9 | export { RMainNavComponent } from './../../component/main-nav'; 10 | // docs 11 | export { RMeterComponent } from './../../component/meter'; 12 | export { RSideNavComponent } from './../../component/side-nav'; 13 | export { RStatsComponent } from './../../component/stats'; 14 | export { AtomComponent } from './../../component/tree/atom'; 15 | export { NodeComponent } from './../../component/tree/node'; 16 | export { TreeComponent } from './../../component/tree/tree'; 17 | export { HomeComponent, render } from './home'; 18 | -------------------------------------------------------------------------------- /src/client/app/view/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@readymade/ui'; 2 | export { RCodeComponent } from './../../component/code'; 3 | export { LibraryComponent, render } from './lib'; 4 | -------------------------------------------------------------------------------- /src/client/app/view/lib/lib.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | font-weight: 400; 4 | font-size: 16px; 5 | padding: 20px; 6 | margin: 0px; 7 | -webkit-font-smoothing: auto; 8 | -moz-osx-font-smoothing: grayscale; 9 | margin-bottom: 360px; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 { 18 | font-family: 'Source Sans Pro', serif; 19 | font-weight: 400; 20 | line-height: 1.5em; 21 | } 22 | 23 | h1 { 24 | font-weight: 700; 25 | } 26 | 27 | h2 { 28 | margin-top: 2em; 29 | } 30 | 31 | h6 { 32 | font-size: 1em; 33 | } 34 | 35 | p { 36 | max-width: 640px; 37 | font-size: 1.2em; 38 | } 39 | 40 | ul { 41 | padding: 0; 42 | margin: 0; 43 | list-style: none; 44 | -webkit-margin-start: 0px; 45 | -webkit-margin-end: 0px; 46 | -webkit-padding-start: 0px; 47 | -webkit-margin-before: 0px; 48 | -webkit-margin-after: 0px; 49 | } 50 | 51 | .theme__toggle { 52 | width: 32px; 53 | height: 32px; 54 | border-radius: 50%; 55 | position: absolute; 56 | top: 40px; 57 | right: 20px; 58 | cursor: pointer; 59 | &.dark { 60 | background: #ffffff; 61 | } 62 | &.light { 63 | background: #000000; 64 | } 65 | } 66 | 67 | header, 68 | section, 69 | footer { 70 | position: relative; 71 | left: 50%; 72 | max-width: 640px; 73 | transform: translateX(-50%); 74 | padding-left: 20px; 75 | padding-right: 20px; 76 | } 77 | 78 | header { 79 | padding-top: 4em; 80 | text-align: center; 81 | padding-bottom: 2em; 82 | } 83 | 84 | header h2 { 85 | font-size: 20px; 86 | font-weight: 300; 87 | margin-top: 2em; 88 | margin-bottom: 2em; 89 | } 90 | 91 | section { 92 | margin-bottom: 20px; 93 | } 94 | 95 | section h1 { 96 | padding-top: 3em; 97 | margin-bottom: 1em; 98 | font-family: 'Source Sans Pro'; 99 | } 100 | 101 | section h2 { 102 | padding: 2px 8px; 103 | background: #cfcfcf; 104 | color: #000000; 105 | font-size: 1.1em; 106 | font-weight: 400; 107 | display: inline-block; 108 | } 109 | 110 | section ul li { 111 | margin-bottom: 0.25em; 112 | } 113 | 114 | .grid { 115 | display: grid; 116 | grid-template-columns: repeat(3, 1fr); 117 | gap: 20px; 118 | } 119 | 120 | .pane { 121 | margin-top: 20px; 122 | } 123 | 124 | .full { 125 | grid-column: span 3; 126 | } 127 | 128 | .definition__list li { 129 | padding-bottom: 0.5em; 130 | } 131 | 132 | .definition__title { 133 | font-style: italic; 134 | color: #ababab; 135 | margin-right: 0.2em; 136 | } 137 | 138 | .i__e { 139 | color: rgba(117, 191, 255, 1); 140 | } 141 | 142 | .i__c { 143 | color: #e6d06c; 144 | } 145 | 146 | rd-surface { 147 | margin-bottom: 40px; 148 | } 149 | 150 | r-code { 151 | width: 100%; 152 | margin-left: -24px; 153 | } 154 | 155 | ul.doc-list { 156 | padding: initial; 157 | margin: initial; 158 | list-style: initial; 159 | -webkit-margin-start: initial; 160 | -webkit-margin-end: initial; 161 | -webkit-padding-start: initial; 162 | -webkit-margin-before: initial; 163 | -webkit-margin-after: initial; 164 | font-size: 1.1em; 165 | margin-left: 20px; 166 | } 167 | -------------------------------------------------------------------------------- /src/client/app/view/perf/index.ts: -------------------------------------------------------------------------------- 1 | export { MyCounter } from '../../component/counter'; 2 | export { PerformanceTestComponent, render } from './perf'; 3 | -------------------------------------------------------------------------------- /src/client/app/view/perf/perf.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Major+Mono+Display|Source Sans Pro:100,300,400'); 2 | 3 | :host { 4 | display: block; 5 | background: #cfcfcf; 6 | color: rgb(25, 25, 25); 7 | font-family: 'Source Sans Pro', sans-serif; 8 | font-weight: 400; 9 | font-size: 16px; 10 | padding: 20px; 11 | margin: 0px; 12 | width: 100%; 13 | height: 100%; 14 | min-height: 100vh; 15 | overflow-y: auto; 16 | -webkit-font-smoothing: auto; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | -------------------------------------------------------------------------------- /src/client/app/view/perf/perf.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | -------------------------------------------------------------------------------- /src/client/app/view/perf/perf.ts: -------------------------------------------------------------------------------- 1 | import { CustomElement, Component } from '@readymade/core'; 2 | 3 | import style from './perf.css?raw'; 4 | import template from './perf.html?raw'; 5 | 6 | @Component({ 7 | selector: 'app-perftest', 8 | style, 9 | template, 10 | }) 11 | class PerformanceTestComponent extends CustomElement { 12 | constructor() { 13 | super(); 14 | } 15 | } 16 | 17 | const render = () => ` 18 | 19 | 25 | 26 | `; 27 | 28 | export { PerformanceTestComponent, render }; 29 | -------------------------------------------------------------------------------- /src/client/app/view/query/index.ts: -------------------------------------------------------------------------------- 1 | export { QueryComponent, render } from './query'; 2 | -------------------------------------------------------------------------------- /src/client/app/view/query/query.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/app/view/query/query.css -------------------------------------------------------------------------------- /src/client/app/view/query/query.html: -------------------------------------------------------------------------------- 1 |
{{params}}
2 | -------------------------------------------------------------------------------- /src/client/app/view/query/query.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement, State } from '@readymade/core'; 2 | import { Route } from '@readymade/router'; 3 | import template from './query.html?raw'; 4 | import style from './query.css?raw'; 5 | 6 | @Component({ 7 | selector: 'app-query', 8 | style, 9 | template, 10 | }) 11 | class QueryComponent extends CustomElement { 12 | constructor() { 13 | super(); 14 | } 15 | 16 | @State() 17 | public getState() { 18 | return { 19 | params: {}, 20 | }; 21 | } 22 | 23 | onNavigate(route: Route) { 24 | this.setState('params', JSON.stringify(route.queryParams)); 25 | } 26 | } 27 | 28 | const render = () => ` 29 | 30 | 36 | 37 | `; 38 | 39 | export { QueryComponent, render }; 40 | -------------------------------------------------------------------------------- /src/client/app/view/test/index.ts: -------------------------------------------------------------------------------- 1 | export { TestBedComponent, render } from './test'; 2 | -------------------------------------------------------------------------------- /src/client/app/view/test/test.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Major+Mono+Display|Source Sans Pro:100,300,400'); 2 | 3 | :host { 4 | display: block; 5 | background: #cfcfcf; 6 | color: rgb(25, 25, 25); 7 | font-family: 'Source Sans Pro', sans-serif; 8 | font-weight: 400; 9 | font-size: 16px; 10 | padding: 20px; 11 | margin: 0px; 12 | width: 100%; 13 | height: 100%; 14 | min-height: 100vh; 15 | overflow-y: auto; 16 | -webkit-font-smoothing: auto; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | ::selection { 21 | background: #ff7de9; /* WebKit/Blink Browsers */ 22 | } 23 | ::-moz-selection { 24 | background: #ff7de9; /* Gecko Browsers */ 25 | } 26 | 27 | r-logo { 28 | margin-bottom: 40px; 29 | } 30 | 31 | header, 32 | section, 33 | footer { 34 | position: relative; 35 | left: 50%; 36 | max-width: 640px; 37 | transform: translateX(-50%); 38 | padding-left: 20px; 39 | padding-right: 20px; 40 | } 41 | 42 | button, 43 | input { 44 | color: white; 45 | font-size: 0.8em; 46 | padding: 10px; 47 | box-sizing: border-box; 48 | text-decoration: none; 49 | outline: none; 50 | box-shadow: 0px 0px 0px transparent; 51 | border: 1px solid transparent; 52 | border-radius: 4px; 53 | transition-property: box-shadow, border; 54 | transition-duration: 300ms; 55 | transition-timing-function: ease-in-out; 56 | } 57 | 58 | ul { 59 | padding: 0; 60 | margin: 0; 61 | list-style: none; 62 | -webkit-margin-start: 0px; 63 | -webkit-margin-end: 0px; 64 | -webkit-padding-start: 0px; 65 | -webkit-margin-before: 0px; 66 | -webkit-margin-after: 0px; 67 | &.is--large { 68 | font-size: 2em; 69 | } 70 | } 71 | 72 | ul li { 73 | margin-left: 10px; 74 | margin-right: 10px; 75 | } 76 | 77 | [tabindex] { 78 | outline: 1px solid transparent; 79 | transition-property: box-shadow, border; 80 | transition-duration: 300ms; 81 | transition-timing-function: ease-in-out; 82 | } 83 | 84 | button, 85 | input { 86 | border-radius: 4px; 87 | outline: none; 88 | box-shadow: 0px 0px 0px transparent; 89 | border: 1px solid transparent; 90 | } 91 | 92 | *:focus, 93 | button:focus, 94 | input:focus { 95 | box-shadow: 0px 0px 0px rgba(255, 105, 180, 1); 96 | outline: 1px solid rgba(255, 105, 180, 1); 97 | } 98 | 99 | [hidden] { 100 | display: none !important; 101 | } 102 | 103 | a:link, 104 | a:visited { 105 | color: #cdcdcd; 106 | } 107 | 108 | a:link:hover, 109 | a:visited:hover { 110 | color: #ffffff; 111 | } 112 | 113 | h1 { 114 | font-family: 'Major Mono Display', serif; 115 | line-height: 1.5em; 116 | } 117 | 118 | h2, 119 | h3, 120 | h4, 121 | h5, 122 | h6 { 123 | font-family: 'Source Sans Pro', serif; 124 | font-weight: 400; 125 | line-height: 1.5em; 126 | } 127 | 128 | h1 { 129 | font-weight: 700; 130 | } 131 | 132 | h6 { 133 | font-size: 1em; 134 | } 135 | 136 | p { 137 | font-family: 'Source Sans Pro', serif; 138 | font-size: 1em; 139 | line-height: 1.5em; 140 | } 141 | 142 | .hint { 143 | opacity: 0.6; 144 | } 145 | 146 | header { 147 | padding-top: 4em; 148 | text-align: center; 149 | padding-bottom: 2em; 150 | } 151 | 152 | header h2 { 153 | font-size: 20px; 154 | font-weight: 300; 155 | margin-top: 2em; 156 | margin-bottom: 2em; 157 | } 158 | 159 | section h1 { 160 | padding-top: 3em; 161 | margin-bottom: 1em; 162 | font-family: 'Source Sans Pro'; 163 | } 164 | 165 | section h2 { 166 | padding: 2px 8px; 167 | background: #cfcfcf; 168 | color: #000000; 169 | font-size: 1.1em; 170 | font-weight: 400; 171 | display: inline-block; 172 | } 173 | 174 | section ul li { 175 | margin-bottom: 0.25em; 176 | } 177 | 178 | .definition__list li { 179 | padding-bottom: 0.5em; 180 | } 181 | 182 | .definition__title { 183 | font-style: italic; 184 | color: #ababab; 185 | margin-right: 0.2em; 186 | } 187 | 188 | .i__e { 189 | color: rgba(117, 191, 255, 1); 190 | } 191 | 192 | .i__c { 193 | color: #e6d06c; 194 | } 195 | 196 | footer { 197 | text-align: center; 198 | margin-top: 60px; 199 | margin-bottom: 60px; 200 | font-size: 2em; 201 | } 202 | 203 | footer p { 204 | font-family: 'Major Mono Display', sans-sarif; 205 | font-size: 0.8em; 206 | } 207 | 208 | footer r-logo { 209 | padding-bottom: 4em; 210 | } 211 | 212 | [is='my-button'] { 213 | background: #181818; 214 | cursor: pointer; 215 | color: #fff; 216 | font-weight: 400; 217 | } 218 | [is='my-input'] { 219 | background: #181818; 220 | border: 0; 221 | color: #fff; 222 | } 223 | 224 | .testbed { 225 | display: flex; 226 | justify-content: space-evenly; 227 | } 228 | -------------------------------------------------------------------------------- /src/client/app/view/test/test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
    6 |
  • 7 | Item 1 8 |
  • 9 |
  • 10 | Item 2 11 |
  • 12 |
  • 13 | Item 3 14 |
  • 15 |
  • 16 | Item 4 17 |
  • 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 |
29 |
30 |
    31 | 34 |
35 | 36 |
    37 | 40 |
41 | 42 | 43 | 44 |
{{message}}
45 |
46 |
47 | -------------------------------------------------------------------------------- /src/client/app/view/test/test.ts: -------------------------------------------------------------------------------- 1 | import { Component, CustomElement, State } from '@readymade/core'; 2 | import { render as renderLogo } from '../../component/logo'; 3 | import template from './test.html?raw'; 4 | import style from './test.css?raw'; 5 | 6 | const objectModel = [ 7 | { 8 | index: 1, 9 | title: 'Item 1', 10 | }, 11 | { 12 | index: 2, 13 | title: 'Item 2', 14 | }, 15 | { 16 | index: 3, 17 | title: 'Item 3', 18 | }, 19 | { 20 | index: 4, 21 | title: 'Item 4', 22 | }, 23 | { 24 | index: 5, 25 | title: 'Item 5', 26 | }, 27 | ]; 28 | 29 | const arrayModel = [1, 'two', 3, 4, 'five']; 30 | 31 | @Component({ 32 | selector: 'app-testbed', 33 | style, 34 | template, 35 | }) 36 | class TestBedComponent extends CustomElement { 37 | constructor() { 38 | super(); 39 | } 40 | 41 | @State() 42 | public getState() { 43 | return { 44 | items: objectModel, 45 | subitems: arrayModel, 46 | message: 'message', 47 | }; 48 | } 49 | } 50 | 51 | const render = () => ` 52 | 53 | 104 | 105 | `; 106 | 107 | export { TestBedComponent, render }; 108 | -------------------------------------------------------------------------------- /src/client/custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Readymade 12 | 13 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/favicon.ico -------------------------------------------------------------------------------- /src/client/hello-state.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Readymade 12 | 13 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/client/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Readymade 12 | 13 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/client/hello.ts: -------------------------------------------------------------------------------- 1 | import { Component, css, CustomElement, html } from '@readymade/core'; 2 | 3 | @Component({ 4 | selector: 'hello-world', 5 | style: css` 6 | :host { 7 | display: block; 8 | } 9 | `, 10 | template: html`

Hello World

`, 11 | }) 12 | class HelloWorldComponent extends CustomElement { 13 | constructor() { 14 | super(); 15 | } 16 | } 17 | 18 | export { HelloWorldComponent }; 19 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Readymade 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 32 |
33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { Router, routing } from './app/routing'; 2 | 3 | if (((import.meta) as any).env.DEV) { 4 | window['clientRouter'] = new Router('#root', routing, true); 5 | } 6 | 7 | export { Router, routing } from './app/routing'; 8 | export { TemplateRepeater, Repeater } from '@readymade/dom'; 9 | export { 10 | MyButtonComponent, 11 | RCodeComponent, 12 | MyCounter, 13 | RHeadlineComponent, 14 | MyInputComponent, 15 | MyItemComponent, 16 | MyListComponent, 17 | RLogoComponent, 18 | RMainNavComponent, 19 | RMeterComponent, 20 | RSideNavComponent, 21 | RStatsComponent, 22 | AtomComponent, 23 | NodeComponent, 24 | TreeComponent, 25 | HomeComponent, 26 | PerformanceTestComponent, 27 | QueryComponent, 28 | TestBedComponent, 29 | LibraryComponent, 30 | } from './app'; 31 | -------------------------------------------------------------------------------- /src/client/performance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Readymade 12 | 13 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/client/polyfill.ts: -------------------------------------------------------------------------------- 1 | import '@ungap/custom-elements'; 2 | -------------------------------------------------------------------------------- /src/client/robots.txt: -------------------------------------------------------------------------------- 1 | # Group 1 2 | User-agent: Googlebot 3 | Disallow: /nogooglebot/ 4 | 5 | # Group 2 6 | User-agent: * 7 | Allow: / 8 | -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Italic/DankMono-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Italic/DankMono-Italic.eot -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Italic/DankMono-Italic.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Italic/DankMono-Italic.svg -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Italic/DankMono-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Italic/DankMono-Italic.ttf -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Italic/DankMono-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Italic/DankMono-Italic.woff -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Italic/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'DankMono-Italic'; 3 | src: url('DankMono-Italic.eot'); 4 | src: 5 | url('DankMono-Italic.woff') format('woff'), 6 | url('DankMono-Italic.ttf') format('truetype'), 7 | url('DankMono-Italic.svg') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Regular/DankMono-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Regular/DankMono-Regular.eot -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Regular/DankMono-Regular.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Regular/DankMono-Regular.svg -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Regular/DankMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Regular/DankMono-Regular.ttf -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Regular/DankMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readymade-ui/readymade/f8c0bae3b42d1ddd0eb2e0a8745a041e983f1002/src/client/style/fonts/DankMono-Regular/DankMono-Regular.woff -------------------------------------------------------------------------------- /src/client/style/fonts/DankMono-Regular/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'DankMono-Regular'; 3 | src: url('DankMono-Regular.eot'); 4 | src: 5 | url('DankMono-Regular.woff') format('woff'), 6 | url('DankMono-Regular.ttf') format('truetype'), 7 | url('DankMono-Regular.svg') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/style/style.css: -------------------------------------------------------------------------------- 1 | @import url('./readymade-ui.css'); 2 | 3 | body { 4 | color: var(--ready-color-default); 5 | background: var(--ready-color-body-bg); 6 | font-family: 'Source Sans Pro', sans-serif; 7 | font-weight: 400; 8 | font-size: 16px; 9 | padding: 0px; 10 | margin: 0px; 11 | width: 100%; 12 | height: 100%; 13 | overflow-y: auto; 14 | -webkit-font-smoothing: auto; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/client/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html'; 2 | declare module '*.css'; 3 | declare module '*.scss'; 4 | -------------------------------------------------------------------------------- /src/client/vendor.ts: -------------------------------------------------------------------------------- 1 | import 'prismjs'; 2 | import 'prismjs/plugins/toolbar/prism-toolbar'; 3 | import 'prismjs/plugins/normalize-whitespace/prism-normalize-whitespace'; 4 | import 'prismjs/components/prism-css'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/components/prism-markup'; 7 | import 'prismjs/components/prism-typescript'; 8 | 9 | // ui library 10 | import '@readymade/core'; 11 | import '@readymade/dom'; 12 | import '@readymade/router'; 13 | import '@readymade/ui'; 14 | -------------------------------------------------------------------------------- /src/modules/core/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephen Belovarich 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. -------------------------------------------------------------------------------- /src/modules/core/README.md: -------------------------------------------------------------------------------- 1 | # readymade 2 | 3 | JavaScript microlibrary for developing Web Components with Decorators that uses only native spec to provide robust features. 4 | 5 | - 🎰 Declare metadata for CSS and HTML ShadowDOM template 6 | - ☕️ Single interface for 'autonomous custom elements' and 'customized built-in elements' 7 | - 🏋️‍ Weighing in ~1.2Kb for 'Hello World' (gzipped) 8 | - 🎤 Event Emitter pattern 9 | - 1️⃣ One-way data binding 10 | - 🖥 Server side renderable 11 | - 🌲 Treeshakable 12 | 13 | Chat with us on [Dischord](https://discord.gg/8GDKfv). 14 | 15 | For more information, read the [Readymade documentation](https://readymade-ui.github.io). 16 | 17 | ### Getting Started 18 | 19 | Install Readymade: 20 | 21 | ``` 22 | npm install @readymade/core 23 | ``` 24 | 25 | If you want to develop with customized built-in elements or Readymade's Repeater components: 26 | 27 | ``` 28 | npm install @readymade/dom 29 | ``` 30 | 31 | If you want to use the client-side router: 32 | 33 | ``` 34 | npm install @readymade/router 35 | ``` 36 | 37 | ### Development 38 | 39 | This repo includes a development server built with Parcel. 40 | 41 | Fork and clone the repo. Install dependencies with yarn. 42 | 43 | ``` 44 | yarn install 45 | ``` 46 | 47 | To develop, run `yarn dev`. This will spin up a local Parcel development server at http://localhost:4444. 48 | 49 | Available routes are specified in src/client/app/router.ts. 50 | 51 | For unit and e2e tests, run `yarn build` then `yarn test`. 52 | 53 | Use `yarn test:open` to open a GUI and run tests interactively. 54 | 55 | ### Production 56 | 57 | To build the library for production, i.e. to use as a local dependency in another project run `yarn build:lib`. 58 | -------------------------------------------------------------------------------- /src/modules/core/decorator/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BIND_SUFFIX, 3 | BoundHandler, 4 | BoundNode, 5 | HANDLER_KEY, 6 | NODE_KEY, 7 | setState, 8 | } from '../element/src/compile'; 9 | import { compileTemplate } from './../element'; 10 | import { EventDispatcher, ReadymadeEventTarget } from './../event'; 11 | 12 | export type EventHandler = () => void; 13 | export const EMIT_KEY = '$emit'; 14 | export const LISTEN_KEY = '$listen'; 15 | 16 | export interface EventMeta { 17 | key: string; 18 | handler: EventHandler; 19 | } 20 | 21 | export interface ElementMeta { 22 | autoDefine?: boolean; 23 | custom?: { 24 | extends: string; 25 | }; 26 | delegatesFocus?: boolean; 27 | eventMap?: { [key: string]: EventMeta }; 28 | mode?: 'closed' | 'open'; 29 | selector: string; 30 | style?: string | any[]; 31 | template?: string | any[]; 32 | } 33 | 34 | export const html = (...args) => { 35 | return args; 36 | }; 37 | 38 | export const css = (...args) => { 39 | return args; 40 | }; 41 | 42 | export const noop = () => {}; 43 | 44 | // Decorators 45 | 46 | export function Component(meta: ElementMeta) { 47 | if (!meta) { 48 | console.error('Component must include ElementMeta to compile'); 49 | return; 50 | } 51 | 52 | return (target: any) => { 53 | compileTemplate(meta, target); 54 | if (meta.autoDefine === undefined) { 55 | meta.autoDefine = true; 56 | } 57 | if ( 58 | meta.autoDefine === true && 59 | customElements.get(meta.selector) === undefined 60 | ) { 61 | if (meta.selector && !meta.custom) { 62 | customElements.define(meta.selector, target); 63 | } else if (meta.selector && meta.custom) { 64 | customElements.define(meta.selector, target, meta.custom); 65 | } else { 66 | customElements.define(meta.selector, target); 67 | } 68 | } 69 | return target; 70 | }; 71 | } 72 | 73 | export function State() { 74 | return function decorator(target: any, key: string | symbol) { 75 | async function bindState() { 76 | this.$state = this[key](); 77 | this.ɵɵstate = {}; 78 | this.ɵɵstate[HANDLER_KEY] = new BoundHandler(this); 79 | this.ɵɵstate[NODE_KEY] = new BoundNode( 80 | this.shadowRoot ? this.shadowRoot : this, 81 | ); 82 | this.ɵɵstate.$changes = new ReadymadeEventTarget(); 83 | this.ɵstate = new Proxy( 84 | this.$state, 85 | this.ɵɵstate['handler' + BIND_SUFFIX], 86 | ); 87 | for (const prop in this.$state) { 88 | this.ɵstate[prop] = this.$state[prop]; 89 | } 90 | } 91 | target.setState = setState; 92 | target.bindState = function onBind() { 93 | bindState.call(this); 94 | }; 95 | }; 96 | } 97 | 98 | export function Emitter( 99 | eventName: string, 100 | options?: any, 101 | channelName?: string, 102 | ) { 103 | return function decorator( 104 | target: any, 105 | propertyKey: string | symbol, 106 | descriptor: PropertyDescriptor, 107 | ) { 108 | const channel = channelName ? channelName : 'default'; 109 | 110 | if (eventName) { 111 | propertyKey = EMIT_KEY + channel + eventName; 112 | } else { 113 | propertyKey = EMIT_KEY + channel; 114 | } 115 | 116 | function addEvent(name?: string, chan?: string) { 117 | if (!this.emitter) { 118 | this.emitter = new EventDispatcher(this, chan); 119 | } 120 | if (name) { 121 | this.emitter.set(name, new CustomEvent(name, options ? options : {})); 122 | } 123 | if (chan && !this.emitter.channels[chan]) { 124 | this.emitter.setChannel(chan); 125 | } 126 | } 127 | 128 | function bindEmitters() { 129 | for (const property in this) { 130 | if (property.includes(EMIT_KEY)) { 131 | this[property].call(this); 132 | } 133 | } 134 | } 135 | 136 | if (!target[propertyKey]) { 137 | target[propertyKey] = function () { 138 | addEvent.call(this, eventName, channelName, descriptor); 139 | }; 140 | } 141 | 142 | target.bindEmitters = function onEmitterInit() { 143 | bindEmitters.call(this); 144 | }; 145 | }; 146 | } 147 | 148 | export function Listen(eventName: string, channelName?: string) { 149 | return function decorator( 150 | target: any, 151 | key: string | number, 152 | descriptor: PropertyDescriptor, 153 | ) { 154 | const symbolHandler = Symbol(key); 155 | 156 | let prop: string = ''; 157 | 158 | if (channelName) { 159 | prop = LISTEN_KEY + eventName + channelName; 160 | } else { 161 | prop = LISTEN_KEY + eventName; 162 | } 163 | 164 | function addListener(name: string, chan: string) { 165 | const handler = (this[symbolHandler] = (...args) => { 166 | descriptor.value.apply(this, args); 167 | }); 168 | if (!this.emitter) { 169 | this.emitter = new EventDispatcher(this, chan ? chan : null); 170 | } 171 | if (!this.elementMeta) { 172 | this.elementMeta = { 173 | eventMap: {}, 174 | }; 175 | } 176 | if (!this.elementMeta.eventMap) { 177 | this.elementMeta.eventMap = {}; 178 | } 179 | if (this.elementMeta) { 180 | this.elementMeta.eventMap[prop] = { 181 | key: name, 182 | handler: key, 183 | }; 184 | } 185 | if (this.addEventListener) { 186 | this.addEventListener(name, handler); 187 | } 188 | } 189 | 190 | function removeListener() { 191 | if (this.removeEventListener) { 192 | this.removeEventListener(eventName, this[symbolHandler]); 193 | } 194 | } 195 | 196 | function addListeners() { 197 | for (const property in this) { 198 | if (property.includes(LISTEN_KEY)) { 199 | this[property].onListener.call(this); 200 | } 201 | } 202 | } 203 | 204 | if (!target[prop]) { 205 | target[prop] = {}; 206 | target[prop].onListener = function onInitWrapper() { 207 | addListener.call(this, eventName, channelName); 208 | }; 209 | target[prop].onDestroyListener = function onDestroyWrapper() { 210 | removeListener.call(this, eventName, channelName); 211 | }; 212 | } 213 | 214 | target.bindListeners = function onListenerInit() { 215 | addListeners.call(this); 216 | }; 217 | }; 218 | } 219 | -------------------------------------------------------------------------------- /src/modules/core/element/index.ts: -------------------------------------------------------------------------------- 1 | export { attachDOM, attachShadow, attachStyle, define } from './src/attach'; 2 | 3 | export { 4 | bindTemplate, 5 | compileTemplate, 6 | templateId, 7 | uuidv4, 8 | } from './src/compile'; 9 | 10 | export { 11 | getSiblings, 12 | getElementIndex, 13 | getParent, 14 | querySelector, 15 | querySelectorAll, 16 | getChildNodes, 17 | isNode, 18 | } from './src/util'; 19 | -------------------------------------------------------------------------------- /src/modules/core/element/src/attach.ts: -------------------------------------------------------------------------------- 1 | import { ElementMeta } from './../../decorator'; 2 | 3 | export function closestRoot(base: Element) { 4 | function __closestFrom(el: any): Element | HTMLHeadElement { 5 | if (el.getRootNode()) { 6 | return el.getRootNode(); 7 | } else { 8 | return document.head; 9 | } 10 | } 11 | return __closestFrom(base); 12 | } 13 | 14 | export function attachShadow(instance: any, options?: any) { 15 | if (!instance.template) { 16 | return; 17 | } 18 | if (!instance.shadowRoot) { 19 | const shadowRoot: ShadowRoot = instance.attachShadow(options || {}); 20 | const t = document.createElement('template'); 21 | t.innerHTML = instance.template; 22 | shadowRoot.appendChild(t.content.cloneNode(true)); 23 | } 24 | instance.bindTemplate(); 25 | } 26 | 27 | export function attachDOM(instance: any) { 28 | if (!instance.elementMeta) { 29 | return; 30 | } 31 | const t = document.createElement('template'); 32 | t.innerHTML = instance.elementMeta.template; 33 | instance.appendChild(t.content.cloneNode(true)); 34 | instance.bindTemplate(); 35 | } 36 | 37 | export function attachStyle(instance: any) { 38 | if (!instance.elementMeta) { 39 | return; 40 | } 41 | const id = `${instance.elementMeta.selector}`; 42 | 43 | const closest: any = closestRoot(instance); 44 | if (closest.tagName === 'HEAD' && document.getElementById(`${id}-x`)) { 45 | return; 46 | } 47 | if (closest.getElementById && closest.getElementById(`${id}-x`)) { 48 | return; 49 | } 50 | const t = document.createElement('style'); 51 | t.setAttribute('id', `${id}-x`); 52 | t.innerText = instance.elementMeta.style; 53 | t.innerText = t.innerText.replace(/:host/gi, `[is=${id}]`); 54 | closest.appendChild(t); 55 | } 56 | 57 | export function define(instance: any, meta: ElementMeta) { 58 | if (meta.autoDefine === true) { 59 | if (meta.selector && !meta.custom) { 60 | customElements.define(meta.selector, instance.contructor); 61 | } else if (meta.selector && meta.custom) { 62 | customElements.define(meta.selector, instance.contructor, meta.custom); 63 | } else { 64 | customElements.define(meta.selector, instance.contructor); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/core/element/src/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export function getParent(el: any) { 3 | return el.parentNode; 4 | } 5 | 6 | export function getChildNodes(template?: any) { 7 | const elem = template ? template : this; 8 | if (!elem) { 9 | return []; 10 | } 11 | function getChildren(node: any, path: any[] = [], result: any[] = []) { 12 | if (!node.children.length) { 13 | result.push(path.concat(node)); 14 | } 15 | for (const child of node.children) { 16 | getChildren(child, path.concat(child), result); 17 | } 18 | return result; 19 | } 20 | const nodes: Element[] = getChildren(elem, []).reduce((nd, curr) => { 21 | return nd.concat(curr); 22 | }, []); 23 | return nodes.filter((item, index) => nodes.indexOf(item) >= index); 24 | } 25 | 26 | export function getSiblings(el: Element) { 27 | return Array.from(getParent(el).children).filter((elem: Element) => { 28 | return elem.tagName !== 'TEXT' && elem.tagName !== 'STYLE'; 29 | }); 30 | } 31 | 32 | export function querySelector(selector: string) { 33 | return document?.querySelector(selector); 34 | } 35 | 36 | export function querySelectorAll(selector: string) { 37 | return Array.from(document.querySelectorAll(selector)); 38 | } 39 | 40 | export function getElementIndex(el: any) { 41 | return getSiblings(el).indexOf(el); 42 | } 43 | 44 | export const isNode = 45 | typeof process === 'object' && String(process) === '[object process]'; 46 | 47 | export const isBrowser = 48 | typeof window !== undefined && typeof window?.document !== undefined; 49 | -------------------------------------------------------------------------------- /src/modules/core/event/index.ts: -------------------------------------------------------------------------------- 1 | // events 2 | import { ElementMeta } from '../decorator'; 3 | 4 | export interface EmitterEvents { 5 | [key: string]: any; 6 | } 7 | 8 | export class ReadymadeEventTarget extends EventTarget {} 9 | 10 | interface ReadymadeElementTarget extends Element { 11 | elementMeta?: ElementMeta; 12 | } 13 | 14 | export class EventDispatcher { 15 | public target: Element; 16 | public events: { 17 | [key: string]: CustomEvent | Event; 18 | }; 19 | public channels: { 20 | [key: string]: BroadcastChannel; 21 | }; 22 | 23 | constructor(context: any, channelName?: string) { 24 | this.target = context; 25 | this.channels = { 26 | default: new BroadcastChannel('default'), 27 | }; 28 | if (channelName) { 29 | this.setChannel(channelName); 30 | } 31 | this.events = {}; 32 | } 33 | public get(eventName: string) { 34 | return this.events[eventName]; 35 | } 36 | 37 | public set(eventName: string, ev: CustomEvent | Event) { 38 | this.events[eventName] = ev; 39 | return this.get(eventName); 40 | } 41 | public emit(ev: Event | string) { 42 | if (typeof ev === 'string') { 43 | ev = this.events[ev]; 44 | } 45 | this.target.dispatchEvent(ev); 46 | } 47 | 48 | public broadcast(ev: CustomEvent | Event | string, name?: string) { 49 | if (typeof ev === 'string') { 50 | ev = this.events[ev]; 51 | } 52 | this.target.dispatchEvent(ev); 53 | const evt = { 54 | bubbles: ev.bubbles, 55 | cancelBubble: ev.cancelBubble, 56 | cancelable: ev.cancelable, 57 | defaultPrevented: ev.defaultPrevented, 58 | detail: (ev as CustomEvent).detail, 59 | timeStamp: ev.timeStamp, 60 | type: ev.type, 61 | }; 62 | if (name) { 63 | this.channels[name].postMessage(evt); 64 | } else { 65 | this.channels.default.postMessage(evt); 66 | } 67 | } 68 | public setChannel(name: string) { 69 | this.channels[name] = new BroadcastChannel(name); 70 | this.channels[name].onmessage = (ev) => { 71 | for (const prop in (this.target as ReadymadeElementTarget).elementMeta 72 | ?.eventMap) { 73 | if (prop.includes(name) && prop.includes(ev.data.type)) { 74 | this.target[(this.target as any).elementMeta?.eventMap[prop].handler]( 75 | ev.data, 76 | ); 77 | } 78 | } 79 | }; 80 | } 81 | public removeChannel(name: string) { 82 | this.channels[name].close(); 83 | delete this.channels[name]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/modules/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event'; 2 | export * from './element'; 3 | export * from './decorator'; 4 | export * from './component'; 5 | -------------------------------------------------------------------------------- /src/modules/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@readymade/core", 3 | "version": "3.1.3", 4 | "description": "JavaScript microlibrary for developing Web Components with TypeScript and Decorators", 5 | "type": "module", 6 | "module": "./fesm2022/index.js", 7 | "typings": "./typings/index.d.ts", 8 | "exports": { 9 | "./package.json": { 10 | "default": "./package.json" 11 | }, 12 | ".": { 13 | "types": "./typings/index.d.ts", 14 | "esm": "./esm2022/index.js", 15 | "esm2022": "./esm2022/index.js", 16 | "default": "./fesm2022/index.js" 17 | } 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readymade-ui/readymade.git" 25 | }, 26 | "keywords": [ 27 | "custom elements", 28 | "web components", 29 | "typescript", 30 | "decorators", 31 | "javascript" 32 | ], 33 | "author": "Stephen Belovarich", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/readymade-ui/readymade/issues" 37 | }, 38 | "homepage": "https://github.com/readymade-ui/readymade#readme" 39 | } -------------------------------------------------------------------------------- /src/modules/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | import terser from '@rollup/plugin-terser'; 5 | 6 | const clean = { 7 | comments: ['none'], 8 | extensions: ['ts', 'js'], 9 | }; 10 | 11 | export default [ 12 | { 13 | input: 'src/modules/core/index.ts', 14 | plugins: [ 15 | resolve(), 16 | typescript({ 17 | sourceMap: false, 18 | declarationDir: 'dist/packages/@readymade/core/fesm2022/typings', 19 | }), 20 | cleanup(clean), 21 | ], 22 | onwarn: (warning, next) => { 23 | if (warning.code === 'THIS_IS_UNDEFINED') return; 24 | next(warning); 25 | }, 26 | output: { 27 | file: 'dist/packages/@readymade/core/fesm2022/index.js', 28 | format: 'esm', 29 | sourcemap: true, 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/modules/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "typings" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/dom/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephen Belovarich 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. -------------------------------------------------------------------------------- /src/modules/dom/README.md: -------------------------------------------------------------------------------- 1 | # @readymade/dom 2 | 3 | Collection of readymade components that extend from native HTML elements. 4 | 5 | ``` 6 | npm install @readymade/dom 7 | ``` 8 | 9 | For more information, read the [Readymade documentation](https://readymade-ui.github.io). 10 | -------------------------------------------------------------------------------- /src/modules/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './repeatr'; 3 | -------------------------------------------------------------------------------- /src/modules/dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@readymade/dom", 3 | "version": "3.1.3", 4 | "description": "JavaScript microlibrary for developing Web Components with TypeScript and Decorators", 5 | "type": "module", 6 | "module": "./fesm2022/index.js", 7 | "typings": "./typings/dom/index.d.ts", 8 | "exports": { 9 | "./package.json": { 10 | "default": "./package.json" 11 | }, 12 | ".": { 13 | "types": "./typings/dom/index.d.ts", 14 | "esm": "./esm2022/dom/index.js", 15 | "esm2022": "./esm2022/dom/index.js", 16 | "default": "./fesm2022/index.js" 17 | } 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readymade-ui/readymade.git" 25 | }, 26 | "keywords": [ 27 | "custom elements", 28 | "web components", 29 | "typescript", 30 | "decorators", 31 | "javascript" 32 | ], 33 | "author": "Stephen Belovarich", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/readymade-ui/readymade/issues" 37 | }, 38 | "homepage": "https://github.com/readymade-ui/readymade#readme" 39 | } -------------------------------------------------------------------------------- /src/modules/dom/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | import terser from '@rollup/plugin-terser'; 5 | 6 | const clean = { 7 | comments: ['none'], 8 | extensions: ['ts', 'js'], 9 | }; 10 | 11 | export default [ 12 | { 13 | input: 'src/modules/dom/index.ts', 14 | plugins: [ 15 | resolve(), 16 | typescript({ 17 | sourceMap: false, 18 | declarationDir: 'dist/packages/@readymade/dom/fesm2022/typings', 19 | }), 20 | cleanup(clean), 21 | ], 22 | onwarn: (warning, next) => { 23 | if (warning.code === 'THIS_IS_UNDEFINED') return; 24 | next(warning); 25 | }, 26 | output: { 27 | file: 'dist/packages/@readymade/dom/fesm2022/index.js', 28 | format: 'esm', 29 | sourcemap: true, 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/modules/dom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "typings" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/router/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephen Belovarich 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. -------------------------------------------------------------------------------- /src/modules/router/README.md: -------------------------------------------------------------------------------- 1 | # @readymade/router 2 | 3 | Client side router compatible with Web Components and Readymade. 4 | 5 | ``` 6 | npm install @readymade/router 7 | ``` 8 | 9 | For more information, read the [Readymade documentation](https://readymade-ui.github.io). 10 | -------------------------------------------------------------------------------- /src/modules/router/index.ts: -------------------------------------------------------------------------------- 1 | import { ElementMeta, EventDispatcher } from '@readymade/core'; 2 | 3 | interface RouteComponent extends HTMLElement { 4 | emitter?: EventDispatcher; 5 | elementMeta?: ElementMeta; 6 | onInit?(): void; 7 | bindEmitters?(): void; 8 | bindListeners?(): void; 9 | bindState?(): void; 10 | 11 | setState?(property: string, model: any): void; 12 | onNavigate?(route: Route): void; 13 | onUpdate?(): void; 14 | onDestroy?(): void; 15 | } 16 | 17 | interface Route { 18 | path: string; 19 | component: string | RouteComponent; 20 | queryParams?: { [key: string]: string }; 21 | title?: string; 22 | description?: string; 23 | 24 | schema?: any; 25 | } 26 | 27 | class Router { 28 | hashMode: boolean; 29 | rootElement: Element; 30 | routes: Array; 31 | currentRoute: Route; 32 | constructor(root: string, routes: Route[], useHash?: boolean) { 33 | if (document.querySelector(root) === null) { 34 | console.error(`[Router] Root element '${root}' does not exist.`); 35 | } 36 | if (!routes) { 37 | console.error(`[Router] initialized without any routes.`); 38 | } 39 | this.rootElement = document.querySelector(root); 40 | this.routes = routes; 41 | if (useHash === true) { 42 | this.hashMode = true; 43 | } else { 44 | this.hashMode = false; 45 | } 46 | this.listen(); 47 | } 48 | 49 | init() { 50 | this.onLocationChange(); 51 | } 52 | 53 | listen() { 54 | if (this.isPushState()) { 55 | window.addEventListener('popstate', this.onLocationChange.bind(this)); 56 | } else if (this.isHashChange()) { 57 | window.addEventListener('hashchange', this.onLocationChange.bind(this)); 58 | } 59 | this.init(); 60 | } 61 | 62 | onLocationChange() { 63 | let path: string; 64 | if (this.hashMode && window.location.hash.length) { 65 | if (window.location.hash === '/#/') { 66 | window.location.href = window.location.href + `/#`; 67 | } else { 68 | path = window.location.hash.replace(/^#/, ''); 69 | } 70 | } else { 71 | if (this.hashMode && !window.location.hash.length) { 72 | window.location.href = 73 | window.location.origin + 74 | window.location.pathname.replace(/\/$/, '') + 75 | `/#/`; 76 | } else { 77 | path = window.location.pathname.replace(/\/$/, ''); 78 | } 79 | } 80 | if (path === '') { 81 | path = '/'; 82 | } 83 | if (this.matchRoute(path)) { 84 | this.navigate(path); 85 | } 86 | } 87 | 88 | decodeQuery() { 89 | if (window.location.search.length === 0) { 90 | return {}; 91 | } 92 | const search = window.location.search.substring(1); 93 | return JSON.parse( 94 | '{"' + 95 | decodeURI(search) 96 | .replace(/"/g, '\\"') 97 | .replace(/&/g, '","') 98 | .replace(/=/g, '":"') + 99 | '"}', 100 | ); 101 | } 102 | 103 | parseQuery(route: Route) { 104 | return new URLSearchParams(route.queryParams); 105 | } 106 | 107 | matchRoute(path: string) { 108 | return this.routes.find((route) => route.path === path); 109 | } 110 | 111 | navigate(path: string) { 112 | const route = this.matchRoute(path); 113 | if (!route) { 114 | console.error(`[Router] Route '${path}' does not exist.`); 115 | return; 116 | } 117 | this.resolve(route); 118 | } 119 | 120 | resolve(route: Route) { 121 | const locationParams = this.decodeQuery(); 122 | const component: RouteComponent = document.createElement( 123 | route.component as string, 124 | ); 125 | 126 | if (Object.keys(locationParams).length) { 127 | route.queryParams = locationParams; 128 | } else if (route.queryParams) { 129 | window.history.replaceState( 130 | {}, 131 | '', 132 | `${location.pathname}?${this.parseQuery(route)}`, 133 | ); 134 | } 135 | 136 | if (route.title) { 137 | document.title = route.title; 138 | } 139 | 140 | if (route.description) { 141 | const description = document.querySelector('meta[name="description"]'); 142 | if (description) { 143 | description.setAttribute('content', route.description); 144 | } 145 | } 146 | 147 | if (route.schema) { 148 | const script = document.querySelector('[type="application/ld+json"]'); 149 | if (script) { 150 | script.innerHTML = route.schema; 151 | } 152 | } 153 | 154 | this.rootElement.innerHTML = ''; 155 | this.rootElement.appendChild(component); 156 | this.currentRoute = route; 157 | 158 | if (component.onNavigate) { 159 | component.onNavigate(this.currentRoute); 160 | } 161 | } 162 | 163 | private isHashChange() { 164 | return typeof window !== 'undefined' && 'onhashchange' in window; 165 | } 166 | 167 | private isPushState() { 168 | return !!( 169 | typeof window !== 'undefined' && 170 | window.history && 171 | window.history.pushState 172 | ); 173 | } 174 | } 175 | 176 | export { Router, Route, RouteComponent }; 177 | -------------------------------------------------------------------------------- /src/modules/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@readymade/router", 3 | "version": "3.1.3", 4 | "description": "JavaScript microlibrary for developing Web Components with TypeScript and Decorators", 5 | "type": "module", 6 | "module": "./fesm2022/index.js", 7 | "typings": "./typings/router/index.d.ts", 8 | "exports": { 9 | "./package.json": { 10 | "default": "./package.json" 11 | }, 12 | ".": { 13 | "types": "./typings/router/index.d.ts", 14 | "esm": "./esm2022/router/index.js", 15 | "esm2022": "./esm2022/router/index.js", 16 | "default": "./fesm2022/index.js" 17 | } 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readymade-ui/readymade.git" 25 | }, 26 | "keywords": [ 27 | "custom elements", 28 | "web components", 29 | "typescript", 30 | "decorators", 31 | "javascript" 32 | ], 33 | "author": "Stephen Belovarich", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/readymade-ui/readymade/issues" 37 | }, 38 | "homepage": "https://github.com/readymade-ui/readymade#readme" 39 | } -------------------------------------------------------------------------------- /src/modules/router/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | import terser from '@rollup/plugin-terser'; 5 | 6 | const clean = { 7 | comments: ['none'], 8 | extensions: ['ts', 'js'], 9 | }; 10 | 11 | export default [ 12 | { 13 | input: 'src/modules/router/index.ts', 14 | plugins: [ 15 | resolve(), 16 | typescript({ 17 | sourceMap: false, 18 | declarationDir: 'dist/packages/@readymade/router/fesm2022/typings', 19 | }), 20 | cleanup(clean), 21 | ], 22 | onwarn: (warning, next) => { 23 | if (warning.code === 'THIS_IS_UNDEFINED') return; 24 | next(warning); 25 | }, 26 | output: { 27 | file: 'dist/packages/@readymade/router/fesm2022/index.js', 28 | format: 'esm', 29 | sourcemap: true, 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/modules/router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "typings" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/transmit/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephen Belovarich 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. -------------------------------------------------------------------------------- /src/modules/transmit/README.md: -------------------------------------------------------------------------------- 1 | # @readymade/transmit 2 | 3 | Swiss-army knife for communicating over WebRTC DataChannel, WebSocket or Touch OSC. 4 | 5 | ```bash 6 | npm install @readymade/transmit 7 | ``` 8 | 9 | ```bash 10 | yarn add @readymade/transmit 11 | ``` 12 | 13 | ## Getting Started 14 | 15 | Import `Transmitter` and instantiate with a configuration Object. 16 | 17 | ```javascript 18 | import { Transmitter, TransmitterConfig } from '@readymade/transmit'; 19 | 20 | const config: TransmitterConfig = { 21 | sharedKey: 'lobby', 22 | rtc: { 23 | iceServers, 24 | }, 25 | serverConfig: { 26 | http: { 27 | protocol: 'http', 28 | hostname: 'localhost', 29 | port: 4449, 30 | }, 31 | ws: { 32 | osc: { 33 | protocol: 'ws', 34 | hostname: 'localhost', 35 | port: 4445, 36 | }, 37 | signal: { 38 | protocol: 'ws', 39 | hostname: 'localhost', 40 | port: 4446, 41 | }, 42 | announce: { 43 | protocol: 'ws', 44 | hostname: 'localhost', 45 | port: 4447, 46 | }, 47 | message: { 48 | protocol: 'ws', 49 | hostname: 'localhost', 50 | port: 4448, 51 | }, 52 | }, 53 | }, 54 | onMessage, 55 | onConnect, 56 | } 57 | 58 | const transmitter = new Transmitter(config); 59 | ``` 60 | 61 | ### Messages 62 | 63 | When `signal` and `announce` servers are configured, the instance of `Transmitter` will automatically attempt a handshake with a remote peer. If a peer is found, a WebRTC DataChannel peer to peer connection will open. To send a message over the data channel use the `send` method. 64 | 65 | ```javascript 66 | transmitter.send({ message: 'ping' }); 67 | ``` 68 | 69 | If you want to send messages over WebSocket, use `sendSocketMessage`. 70 | 71 | ```javascript 72 | transmitter.sendSocketMessage({ message: 'ping' }); 73 | ``` 74 | 75 | To send a message over TouchOSC, use `sendTouchOSCMessage`, ensuring the data your are sending follows the OSC protocol. Below is an example of sending a OSC message with type definitions. 76 | 77 | ```javascript 78 | transmitter.sendTouchOSCMessage('/OSCQUERY/Left Controls/Flip H', [ 79 | { 80 | type: 'i', 81 | value: 1, 82 | }, 83 | ]); 84 | ``` 85 | 86 | To listen for messages, inject a callback into the configuration. In the above example, `onMessage` would appear like so: 87 | 88 | ```javascript 89 | const onMessage = (message) => { 90 | if (message.payload.event === 'ping') { 91 | this.transmitter.send({ event: 'pong' }); 92 | } 93 | }; 94 | ``` 95 | 96 | To react to a peer to peer connection, bind an `onConnect` callback to the configuration. 97 | 98 | ## transit-server 99 | 100 | For plug and play functionality use a Readymade `transmit-server`, a Node.js server that provides a WebRTC signaling server, WebSocket messaging channel, and WebSocket bridge for communicating over TouchOSC. 101 | 102 | `transmit-server` is included in the Readymade starter code. Create a new Readymade project using the command `npx primr my-app`, renaming the directory `my-app` with your project name. `transmit-server` will be included in the project directory. After installing dependencies, run `yarn build:transmit` and `yarn serve:transmit`. Automatically, the WebSocket and Express servers should instantiate like so: 103 | 104 | ```bash 105 | yarn serve:transmit 106 | Express Server is listening on http://localhost:4449 107 | Free ICE servers available by making a GET request to http://localhost:4449/ice 108 | TouchOSC Message Server is listening on http://localhost:4445 109 | Signal Server is listening on http://localhost:4446 110 | Announce Server is listening on http://localhost:4447 111 | Message Server is listening on http://localhost:4448 112 | ``` 113 | 114 | For more information about `primr`, read the [Readymade documentation](https://readymade-ui.github.io). 115 | -------------------------------------------------------------------------------- /src/modules/transmit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@readymade/transmit", 3 | "version": "3.1.3", 4 | "description": "Swiss-army knife for communicating over WebRTC DataChannel, WebSocket or Touch OSC", 5 | "type": "module", 6 | "module": "./fesm2022/index.js", 7 | "typings": "./typings/index.d.ts", 8 | "exports": { 9 | "./package.json": { 10 | "default": "./package.json" 11 | }, 12 | ".": { 13 | "types": "./typings/index.d.ts", 14 | "esm": "./esm2022/index.js", 15 | "esm2022": "./esm2022/index.js", 16 | "default": "./fesm2022/index.js" 17 | } 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readymade-ui/readymade.git" 25 | }, 26 | "keywords": [ 27 | "javascript", 28 | "touchosc", 29 | "osc", 30 | "websocket", 31 | "web socket", 32 | "webrtc" 33 | ], 34 | "author": "Stephen Belovarich", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/readymade-ui/readymade/issues" 38 | }, 39 | "homepage": "https://github.com/readymade-ui/readymade#readme" 40 | } -------------------------------------------------------------------------------- /src/modules/transmit/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | 5 | const clean = { 6 | comments: ['none'], 7 | extensions: ['ts', 'js'], 8 | }; 9 | 10 | export default [ 11 | { 12 | input: 'src/modules/transmit/index.ts', 13 | plugins: [ 14 | resolve(), 15 | typescript({ 16 | sourceMap: false, 17 | declarationDir: 'dist/packages/@readymade/transmit/fesm2022/typings', 18 | }), 19 | cleanup(clean), 20 | ], 21 | onwarn: (warning, next) => { 22 | if (warning.code === 'THIS_IS_UNDEFINED') return; 23 | next(warning); 24 | }, 25 | output: { 26 | file: 'dist/packages/@readymade/transmit/fesm2022/index.js', 27 | format: 'esm', 28 | sourcemap: true, 29 | }, 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/modules/transmit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "typings" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/ui/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephen Belovarich 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. -------------------------------------------------------------------------------- /src/modules/ui/README.md: -------------------------------------------------------------------------------- 1 | # @readymade/ui 2 | 3 | UI library of standard elements built with Readymade. 4 | 5 | ``` 6 | npm install @readymade/ui 7 | ``` 8 | 9 | For more information, read the [Readymade documentation](https://readymade-ui.github.io). 10 | -------------------------------------------------------------------------------- /src/modules/ui/component/control.ts: -------------------------------------------------------------------------------- 1 | export interface RdLegacyControl { 2 | type: string; 3 | name: string; 4 | selector: string; 5 | orient?: string; 6 | stops?: number[]; 7 | min?: number | number[]; 8 | max?: number | number[]; 9 | isActive?: boolean; 10 | hasUserInput?: boolean; 11 | hasRemoteInput?: boolean; 12 | currentValue?: number | string | Array | Array; 13 | position?: string; 14 | x?: number; 15 | y?: number; 16 | height?: number; 17 | width?: number; 18 | size?: string; 19 | timeStamp?: Date | number; 20 | snapToCenter?: boolean; 21 | gridArea?: string; 22 | placeSelf?: string; 23 | transform?: string; 24 | numberType?: 'int' | 'float'; 25 | label?: string; 26 | channel?: string; 27 | } 28 | 29 | export interface RdControl { 30 | type?: string; 31 | name: string; 32 | isActive?: boolean; 33 | hasUserInput?: boolean; 34 | hasRemoteInput?: boolean; 35 | currentValue?: number | string | Array | Array | boolean; 36 | timeStamp?: Date | number; 37 | attributes?: A; 38 | } 39 | 40 | export interface RdControlSurfaceElement { 41 | label?: string; 42 | selector: string; 43 | style?: Partial; 44 | classes?: Array; 45 | control: C; 46 | channel?: string; 47 | hint?: { 48 | template: string; 49 | }; 50 | displayValue?: boolean; 51 | } 52 | 53 | export interface RdControlSurface { 54 | label?: string; 55 | name?: string; 56 | style?: Partial; 57 | classes?: Array; 58 | controls: Array>; 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/ui/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './control'; 2 | export { RdButton } from './input/button'; 3 | export { 4 | RdButtonPad, 5 | StandardKeyboard, 6 | StandardKeyboardModifiers, 7 | StandardKeyboardNumPad, 8 | StandardKeyboardModifierCodeKeyMap, 9 | } from './input/buttonpad'; 10 | export { RdSwitch } from './input/switch'; 11 | export { RdInput } from './input/input'; 12 | export { RdRadioGroup } from './input/radio'; 13 | export { RdCheckBox } from './input/checkbox'; 14 | export { RdTextArea } from './input/textarea'; 15 | export { RdDropdown } from './input/select'; 16 | export { RdSlider } from './input/slider'; 17 | export { RdDial } from './input/dial'; 18 | export { RdSurface, RdSurfaceElement, RdDisplayInput } from './surface'; 19 | -------------------------------------------------------------------------------- /src/modules/ui/component/input/checkbox.ts: -------------------------------------------------------------------------------- 1 | import { Component, Emitter, FormElement, html, css } from '@readymade/core'; 2 | import { RdControl } from '../control'; 3 | 4 | export interface RdCheckboxAttributes { 5 | checked?: boolean; 6 | } 7 | 8 | @Component({ 9 | selector: 'rd-checkbox', 10 | delegatesFocus: true, 11 | style: css` 12 | :host { 13 | display: inline-block; 14 | width: 28px; 15 | height: 28px; 16 | outline: none; 17 | } 18 | :host input[type='checkbox'] { 19 | -moz-appearance: none; 20 | -webkit-appearance: none; 21 | appearance: none; 22 | margin: 0; 23 | } 24 | :host input[type='checkbox']:before { 25 | content: ''; 26 | display: block; 27 | width: 24px; 28 | height: 24px; 29 | border: var(--ready-border-width) solid var(--ready-color-border); 30 | border-radius: 6px; 31 | background: var(--ready-color-bg); 32 | } 33 | :host input[type='checkbox']:checked:before { 34 | background-image: var(--ready-icon-check); 35 | background-repeat: no-repeat; 36 | background-position: center; 37 | } 38 | :host input[type='checkbox']:focus, 39 | :host input[type='checkbox']:active { 40 | outline: 0px; 41 | outline-offset: 0px; 42 | } 43 | :host input[type='checkbox']:hover:before, 44 | :host input[type='checkbox']:focus:before, 45 | :host input[type='checkbox']:active:before { 46 | border: var(--ready-border-width) solid var(--ready-color-highlight); 47 | } 48 | :host input[type='checkbox'][disabled]:before { 49 | opacity: var(--ready-opacity-disabled); 50 | background: var(--ready-color-disabled); 51 | cursor: not-allowed; 52 | } 53 | :host input[type='checkbox'][disabled]:checked:before { 54 | background-image: var(--ready-icon-check); 55 | background-repeat: no-repeat; 56 | background-position: center; 57 | } 58 | :host input[type='checkbox'][disabled]:hover:before, 59 | :host input[type='checkbox'][disabled]:focus:before, 60 | :host input[type='checkbox'][disabled]:active:before { 61 | border: var(--ready-border-width) solid var(--ready-color-border); 62 | outline: none; 63 | box-shadow: none; 64 | } 65 | :host input[type='checkbox'].required:before, 66 | :host input[type='checkbox'].required:hover:before, 67 | :host input[type='checkbox'].required:focus:before, 68 | :host input[type='checkbox'].required:active:before { 69 | border: var(--ready-border-width) solid var(--ready-color-error); 70 | outline: none; 71 | box-shadow: none; 72 | } 73 | `, 74 | template: html` `, 75 | }) 76 | class RdCheckBox extends FormElement { 77 | channel: BroadcastChannel; 78 | control: RdControl; 79 | constructor() { 80 | super(); 81 | } 82 | 83 | static get observedAttributes() { 84 | return ['checked', 'channel', 'control']; 85 | } 86 | 87 | attributeChangedCallback(name: string, old: string, next: string) { 88 | switch (name) { 89 | case 'checked': 90 | this.checked = next === 'true' || next === '' ? true : false; 91 | break; 92 | case 'channel': 93 | this.setChannel(next); 94 | break; 95 | case 'control': 96 | if (!next.startsWith('{{')) { 97 | this.setControl(JSON.parse(next)); 98 | } 99 | break; 100 | } 101 | } 102 | 103 | formDisabledCallback(disabled: boolean) { 104 | this.$elem.disabled = disabled; 105 | } 106 | 107 | formResetCallback() { 108 | this.$elem.checked = false; 109 | } 110 | 111 | onValidate() { 112 | if (this.hasAttribute('required') && this.value === false) { 113 | this.$internals.setValidity({ customError: true }, 'required'); 114 | this.$elem.classList.add('required'); 115 | } else { 116 | this.$internals.setValidity({}); 117 | this.$elem.classList.remove('required'); 118 | } 119 | } 120 | 121 | @Emitter('change') 122 | connectedCallback() { 123 | this.$elem.onchange = (ev: Event) => { 124 | if (this.onchange) { 125 | this.onchange(ev); 126 | } else { 127 | this.emitter.emit( 128 | new CustomEvent('change', { 129 | bubbles: true, 130 | composed: true, 131 | detail: 'composed', 132 | }), 133 | ); 134 | } 135 | if (this.channel) { 136 | this.control.currentValue = (ev.target as HTMLInputElement).checked; 137 | if (this.control.attributes) { 138 | this.control.attributes.checked = ( 139 | ev.target as HTMLInputElement 140 | ).checked; 141 | } 142 | this.channel.postMessage(this.control); 143 | } 144 | }; 145 | this.$elem.onblur = () => { 146 | this.onValidate(); 147 | }; 148 | } 149 | 150 | get type() { 151 | return 'checkbox'; 152 | } 153 | 154 | get form() { 155 | return this.$internals.form; 156 | } 157 | 158 | get name() { 159 | return this.getAttribute('name'); 160 | } 161 | 162 | checkValidity() { 163 | return this.$internals.checkValidity(); 164 | } 165 | 166 | get validity() { 167 | return this.$internals.validity; 168 | } 169 | 170 | get validationMessage() { 171 | return this.$internals.validationMessage; 172 | } 173 | 174 | get willValidate() { 175 | return this.$internals.willValidate; 176 | } 177 | 178 | get checked(): boolean { 179 | return this.$elem.checked; 180 | } 181 | 182 | set checked(value) { 183 | this.$elem.checked = value; 184 | } 185 | 186 | get value(): boolean { 187 | return this.$elem.checked; 188 | } 189 | 190 | set value(value) { 191 | if (typeof value === 'boolean') { 192 | this.$elem.checked = value; 193 | if (this.control) { 194 | this.control.currentValue = value; 195 | if (this.control.attributes.checked) { 196 | this.control.attributes.checked = value; 197 | } 198 | } 199 | } 200 | } 201 | 202 | get $elem(): HTMLInputElement { 203 | return this.shadowRoot.querySelector('input'); 204 | } 205 | 206 | setChannel(name: string) { 207 | this.channel = new BroadcastChannel(name); 208 | } 209 | 210 | setControl(control: RdControl) { 211 | this.control = control; 212 | this.setAttribute('name', control.name); 213 | if ( 214 | (control.currentValue && typeof control.currentValue === 'boolean') || 215 | control.attributes.checked 216 | ) { 217 | this.checked = control.currentValue 218 | ? Boolean(control.currentValue) 219 | : (control.attributes.checked as boolean); 220 | } 221 | } 222 | } 223 | 224 | export { RdCheckBox }; 225 | -------------------------------------------------------------------------------- /src/modules/ui/component/input/input.ts: -------------------------------------------------------------------------------- 1 | import { Component, Emitter, FormElement, html, css } from '@readymade/core'; 2 | import { RdControl } from '../control'; 3 | 4 | export interface RdInputAttributes { 5 | value: string; 6 | } 7 | 8 | @Component({ 9 | selector: 'rd-input', 10 | delegatesFocus: true, 11 | style: css` 12 | :host { 13 | display: inline-block; 14 | outline: none; 15 | } 16 | :host input { 17 | width: 100%; 18 | background-color: var(--ready-color-bg); 19 | border: var(--ready-border-width) solid var(--ready-color-border); 20 | border-radius: var(--ready-border-radius); 21 | color: var(--ready-color-default); 22 | font: var(--font-family); 23 | min-height: 2em; 24 | padding: 0em 1em; 25 | } 26 | :host input:hover, 27 | :host input:focus, 28 | :host input:active { 29 | border: var(--ready-border-width) solid var(--ready-color-highlight); 30 | outline: none; 31 | box-shadow: none; 32 | } 33 | :host input[disabled] { 34 | opacity: var(--ready-opacity-disabled); 35 | background: var(--ready-color-disabled); 36 | cursor: not-allowed; 37 | } 38 | :host input[disabled]:hover, 39 | :host input[disabled]:focus, 40 | :host input[disabled]:active { 41 | border: var(--ready-border-width) solid var(--ready-color-border); 42 | outline: none; 43 | box-shadow: none; 44 | } 45 | :host input.required, 46 | :host input.required:hover, 47 | :host input.required:focus, 48 | :host input.required:active { 49 | border: var(--ready-border-width) solid var(--ready-color-error); 50 | outline: none; 51 | box-shadow: none; 52 | } 53 | `, 54 | template: html` `, 55 | }) 56 | class RdInput extends FormElement { 57 | channel: BroadcastChannel; 58 | control: RdControl; 59 | constructor() { 60 | super(); 61 | } 62 | 63 | static get observedAttributes() { 64 | return ['channel']; 65 | } 66 | 67 | attributeChangedCallback(name: string, old: string, next: string) { 68 | switch (name) { 69 | case 'channel': 70 | this.setChannel(next); 71 | break; 72 | case 'control': 73 | if (!next.startsWith('{{')) { 74 | this.setControl(JSON.parse(next)); 75 | } 76 | break; 77 | } 78 | } 79 | 80 | @Emitter('change') 81 | connectedCallback() { 82 | this.$elem.onchange = (ev: Event) => { 83 | if (this.onchange) { 84 | this.onchange(ev); 85 | } 86 | }; 87 | this.$elem.oninput = (ev: Event) => { 88 | this.emitter.emit( 89 | new CustomEvent('change', { 90 | bubbles: true, 91 | composed: true, 92 | detail: 'composed', 93 | }), 94 | ); 95 | if (this.oninput) { 96 | this.oninput(ev); 97 | } 98 | if (this.channel) { 99 | this.control.currentValue = this.value; 100 | this.control.attributes.value = this.value; 101 | this.channel.postMessage(this.control); 102 | } 103 | }; 104 | this.$elem.onblur = () => { 105 | this.onValidate(); 106 | }; 107 | } 108 | 109 | formDisabledCallback(disabled: boolean) { 110 | this.$elem.disabled = disabled; 111 | } 112 | 113 | formResetCallback() { 114 | this.value = ''; 115 | this.$internals.setFormValue(''); 116 | } 117 | 118 | onValidate() { 119 | if (this.hasAttribute('required') && this.value.length <= 0) { 120 | this.$internals.setValidity({ customError: true }, 'required'); 121 | this.$elem.classList.add('required'); 122 | } else { 123 | this.$internals.setValidity({}); 124 | this.$elem.classList.remove('required'); 125 | } 126 | } 127 | 128 | get type() { 129 | return 'text'; 130 | } 131 | 132 | get form() { 133 | return this.$internals.form; 134 | } 135 | 136 | get name() { 137 | return this.getAttribute('name'); 138 | } 139 | 140 | checkValidity() { 141 | return this.$internals.checkValidity(); 142 | } 143 | 144 | get validity() { 145 | return this.$internals.validity; 146 | } 147 | 148 | get validationMessage() { 149 | return this.$internals.validationMessage; 150 | } 151 | 152 | get willValidate() { 153 | return this.$internals.willValidate; 154 | } 155 | 156 | get value(): string { 157 | return this.$elem.value; 158 | } 159 | 160 | set value(value) { 161 | this.$elem.value = value; 162 | if (this.control) { 163 | this.control.currentValue = value; 164 | this.control.attributes.value = value; 165 | } 166 | } 167 | 168 | get $elem(): HTMLInputElement | HTMLTextAreaElement { 169 | return this.shadowRoot.querySelector('input'); 170 | } 171 | 172 | setChannel(name: string) { 173 | this.channel = new BroadcastChannel(name); 174 | } 175 | 176 | setControl(control: RdControl) { 177 | this.control = control; 178 | this.setAttribute('name', control.name); 179 | this.setAttribute('type', control.type); 180 | if (control.currentValue && typeof control.currentValue === 'string') { 181 | this.value = control.currentValue as string; 182 | } 183 | } 184 | } 185 | 186 | export { RdInput }; 187 | -------------------------------------------------------------------------------- /src/modules/ui/component/input/select.ts: -------------------------------------------------------------------------------- 1 | import { Component, Emitter, FormElement, css, html } from '@readymade/core'; 2 | import { RdControl } from '../control'; 3 | 4 | export interface RdDropdownAttributes { 5 | options?: Array; 6 | } 7 | 8 | @Component({ 9 | selector: 'rd-dropdown', 10 | delegatesFocus: true, 11 | style: css` 12 | :host { 13 | display: inline-block; 14 | outline: none; 15 | } 16 | ::slotted(select) { 17 | display: block; 18 | width: 100%; 19 | background-color: var(--ready-color-bg); 20 | border: var(--ready-border-width) solid var(--ready-color-border); 21 | border-radius: var(--ready-border-radius); 22 | color: var(--ready-color-default); 23 | font: var(--font-family); 24 | line-height: 1.3; 25 | padding: 0.3em 1.6em 0.3em 0.8em; 26 | height: 36px; 27 | box-sizing: border-box; 28 | margin: 0; 29 | -moz-appearance: none; 30 | -webkit-appearance: none; 31 | appearance: none; 32 | background-image: var(--ready-icon-menu); 33 | background-repeat: no-repeat; 34 | background-position: 35 | right 0.7em top 50%, 36 | 0 0; 37 | background-size: 10px 9px; 38 | } 39 | ::slotted(select:hover), 40 | ::slotted(select:focus), 41 | ::slotted(select:active) { 42 | border: var(--ready-border-width) solid var(--ready-color-highlight); 43 | outline: none; 44 | box-shadow: none; 45 | } 46 | *[dir='rtl'] ::slotted(select), 47 | :root:lang(ar) ::slotted(select), 48 | :root:lang(iw) ::slotted(select) { 49 | background-position: 50 | left 0.7em top 50%, 51 | 0 0; 52 | padding: 0.3em 0.8em 0.3em 1.4em; 53 | } 54 | ::slotted(select::-ms-expand) { 55 | display: none; 56 | } 57 | ::slotted(select[disabled]) { 58 | opacity: var(--ready-opacity-disabled); 59 | background: var(--ready-color-disabled); 60 | background-image: var(--ready-icon-menu); 61 | background-repeat: no-repeat; 62 | background-position: 63 | right 0.7em top 50%, 64 | 0 0; 65 | background-size: 10px 9px; 66 | cursor: not-allowed; 67 | } 68 | ::slotted(select[disabled]:hover), 69 | ::slotted(select[disabled]:focus), 70 | ::slotted(select[disabled]:active) { 71 | border: var(--ready-border-width) solid var(--ready-color-border); 72 | outline: none; 73 | box-shadow: none; 74 | } 75 | ::slotted(select.required), 76 | ::slotted(select.required:hover), 77 | ::slotted(select.required:focus), 78 | ::slotted(select.required:active) { 79 | border: var(--ready-border-width) solid var(--ready-color-error); 80 | outline: none; 81 | box-shadow: none; 82 | } 83 | `, 84 | template: html` `, 85 | }) 86 | class RdDropdown extends FormElement { 87 | channel: BroadcastChannel; 88 | control: RdControl; 89 | constructor() { 90 | super(); 91 | } 92 | 93 | static get observedAttributes() { 94 | return ['channel']; 95 | } 96 | 97 | attributeChangedCallback(name: string, old: string, next: string) { 98 | switch (name) { 99 | case 'channel': 100 | this.setChannel(next); 101 | break; 102 | case 'control': 103 | if (!next.startsWith('{{')) { 104 | this.setControl(JSON.parse(next)); 105 | } 106 | break; 107 | } 108 | } 109 | 110 | @Emitter('select') 111 | connectedCallback() { 112 | this.$elem.oninput = (ev: Event) => { 113 | this.emitter.emit( 114 | new CustomEvent('select', { 115 | bubbles: true, 116 | composed: true, 117 | detail: 'composed', 118 | }), 119 | ); 120 | if (this.onselect) { 121 | this.onselect(ev); 122 | } 123 | if (this.oninput) { 124 | this.oninput(ev); 125 | } 126 | if (this.channel) { 127 | this.control.currentValue = (ev.target as HTMLSelectElement).value; 128 | this.channel.postMessage(this.control); 129 | } 130 | }; 131 | this.$elem.onblur = () => { 132 | this.onValidate(); 133 | }; 134 | } 135 | 136 | formDisabledCallback(disabled: boolean) { 137 | this.$elem.disabled = disabled; 138 | } 139 | 140 | formResetCallback() { 141 | this.$elem.selectedIndex = -1; 142 | this.$internals.setFormValue(''); 143 | } 144 | 145 | onValidate() { 146 | if (this.hasAttribute('required') && this.value.length <= 0) { 147 | this.$internals.setValidity({ customError: true }, 'required'); 148 | this.$elem.classList.add('required'); 149 | } else { 150 | this.$internals.setValidity({}); 151 | this.$elem.classList.remove('required'); 152 | } 153 | } 154 | 155 | get form() { 156 | return this.$internals.form; 157 | } 158 | 159 | get name() { 160 | return this.getAttribute('name'); 161 | } 162 | 163 | checkValidity() { 164 | return this.$internals.checkValidity(); 165 | } 166 | 167 | get validity() { 168 | return this.$internals.validity; 169 | } 170 | 171 | get validationMessage() { 172 | return this.$internals.validationMessage; 173 | } 174 | 175 | get willValidate() { 176 | return this.$internals.willValidate; 177 | } 178 | 179 | get value(): string { 180 | return this.$elem.value; 181 | } 182 | 183 | set value(value) { 184 | this.$elem.value = value; 185 | if (this.control) { 186 | this.control.currentValue = value; 187 | } 188 | } 189 | 190 | get $elem(): HTMLSelectElement { 191 | return ( 192 | this.shadowRoot 193 | .querySelector('slot') 194 | .assignedNodes() as HTMLSelectElement[] 195 | ).filter((elem) => elem.tagName === 'SELECT')[0]; 196 | } 197 | 198 | setChannel(name: string) { 199 | this.channel = new BroadcastChannel(name); 200 | } 201 | 202 | setControl(control: RdControl) { 203 | this.control = control; 204 | this.setAttribute('name', control.name); 205 | this.setAttribute('type', control.type); 206 | if (control.attributes.options) { 207 | this.innerHTML = ''; 208 | const select = document.createElement('select'); 209 | 210 | const defaultOption = document.createElement('option'); 211 | defaultOption.value = ''; 212 | defaultOption.text = 'Select an option'; 213 | select.appendChild(defaultOption); 214 | 215 | for (let i = 0; i < control.attributes.options.length; i++) { 216 | const option = document.createElement('option'); 217 | option.textContent = control.attributes.options[i]; 218 | select.appendChild(option); 219 | } 220 | this.appendChild(select); 221 | } 222 | if (control.currentValue && typeof control.currentValue === 'string') { 223 | this.value = control.currentValue as string; 224 | } 225 | } 226 | } 227 | 228 | export { RdDropdown }; 229 | -------------------------------------------------------------------------------- /src/modules/ui/component/input/switch.ts: -------------------------------------------------------------------------------- 1 | import { Component, html, css } from '@readymade/core'; 2 | import { RdCheckBox } from './checkbox'; 3 | 4 | @Component({ 5 | selector: 'rd-switch', 6 | delegatesFocus: true, 7 | style: css` 8 | :host { 9 | display: inline-block; 10 | width: 72px; 11 | height: 36px; 12 | outline: none; 13 | } 14 | :host input[type='checkbox'] { 15 | display: flex; 16 | width: 72px; 17 | height: 36px; 18 | -moz-appearance: none; 19 | -webkit-appearance: none; 20 | appearance: none; 21 | margin: 0; 22 | } 23 | :host input[type='checkbox']:before { 24 | content: ''; 25 | width: 100%; 26 | border: var(--ready-border-width) solid var(--ready-color-border); 27 | background-color: var(--ready-color-bg); 28 | border-radius: var(--ready-border-radius); 29 | color: var(--ready-color-default); 30 | padding: 1px 0px; 31 | background-image: var(--ready-icon-switch); 32 | background-size: 22px 22px; 33 | background-repeat: no-repeat; 34 | background-position: left var(--ready-border-width) top 50%; 35 | } 36 | :host input[type='checkbox']:checked:before { 37 | background-image: var(--ready-icon-switch); 38 | background-size: 22px 22px; 39 | background-repeat: no-repeat; 40 | background-position: right var(--ready-border-width) top 50%; 41 | } 42 | :host input[type='checkbox']:hover:before, 43 | :host input[type='checkbox']:focus:before, 44 | :host input[type='checkbox']:active:before { 45 | border: var(--ready-border-width) solid var(--ready-color-highlight); 46 | } 47 | :host input[type='checkbox']:focus, 48 | :host input[type='checkbox']:active { 49 | outline: 0px; 50 | outline-offset: 0px; 51 | } 52 | :host input[type='checkbox']:active:before { 53 | background-color: var(--ready-color-selected); 54 | border: var(--ready-border-width) solid var(--ready-color-highlight); 55 | } 56 | :host input[type='checkbox'][disabled]:before { 57 | opacity: var(--ready-opacity-disabled); 58 | background: var(--ready-color-disabled); 59 | background-image: var(--ready-icon-switch); 60 | background-size: 22px 22px; 61 | background-repeat: no-repeat; 62 | background-position: left var(--ready-border-width) top 50%; 63 | cursor: not-allowed; 64 | } 65 | :host input[type='checkbox'][disabled]:checked:before { 66 | background-image: var(--ready-icon-switch); 67 | background-size: 22px 22px; 68 | background-repeat: no-repeat; 69 | background-position: right var(--ready-border-width) top 50%; 70 | } 71 | :host input[type='checkbox'][disabled]:hover:before, 72 | :host input[type='checkbox'][disabled]:focus:before, 73 | :host input[type='checkbox'][disabled]:active:before { 74 | border: var(--ready-border-width) solid var(--ready-color-border); 75 | outline: none; 76 | box-shadow: none; 77 | } 78 | :host input[type='checkbox'].required:before, 79 | :host input[type='checkbox'].required:hover:before, 80 | :host input[type='checkbox'].required:focus:before, 81 | :host input[type='checkbox'].required:active:before { 82 | border: var(--ready-border-width) solid var(--ready-color-error); 83 | outline: none; 84 | box-shadow: none; 85 | } 86 | `, 87 | template: html` `, 88 | }) 89 | class RdSwitch extends RdCheckBox { 90 | constructor() { 91 | super(); 92 | } 93 | } 94 | 95 | export { RdSwitch }; 96 | -------------------------------------------------------------------------------- /src/modules/ui/component/input/textarea.ts: -------------------------------------------------------------------------------- 1 | import { Component, html, css } from '@readymade/core'; 2 | import { RdInput } from './input'; 3 | 4 | @Component({ 5 | selector: 'rd-textarea', 6 | delegatesFocus: true, 7 | style: css` 8 | :host { 9 | display: inline-block; 10 | outline: none; 11 | } 12 | :host textarea { 13 | background-color: var(--ready-color-bg); 14 | border: var(--ready-border-width) solid var(--ready-color-border); 15 | border-radius: var(--ready-border-radius); 16 | color: var(--ready-color-default); 17 | font: var(--font-family); 18 | outline: none; 19 | overflow: auto; 20 | padding: 1em; 21 | -moz-appearance: none; 22 | -webkit-appearance: none; 23 | appearance: none; 24 | background-image: var(--ready-icon-expand); 25 | background-position: bottom 0.5em right 0.5em; 26 | background-repeat: no-repeat; 27 | } 28 | :host textarea:hover, 29 | :host textarea:focus, 30 | :host textarea:active { 31 | border: var(--ready-border-width) solid var(--ready-color-highlight); 32 | outline: none; 33 | box-shadow: none; 34 | } 35 | :host textarea[disabled] { 36 | opacity: var(--ready-opacity-disabled); 37 | background: var(--ready-color-disabled); 38 | cursor: not-allowed; 39 | } 40 | :host textarea[disabled]:hover, 41 | :host textarea[disabled]:focus, 42 | :host textarea[disabled]:active { 43 | border: var(--ready-border-width) solid var(--ready-color-border); 44 | outline: none; 45 | box-shadow: none; 46 | } 47 | :host textarea.required, 48 | :host textarea.required:hover, 49 | :host textarea.required:focus, 50 | :host textarea.required:active { 51 | border: var(--ready-border-width) solid var(--ready-color-error); 52 | outline: none; 53 | box-shadow: none; 54 | } 55 | textarea::-webkit-resizer { 56 | display: none; 57 | } 58 | `, 59 | template: html` `, 60 | }) 61 | class RdTextArea extends RdInput { 62 | constructor() { 63 | super(); 64 | } 65 | get $elem() { 66 | return this.shadowRoot.querySelector('textarea'); 67 | } 68 | } 69 | 70 | export { RdTextArea }; 71 | -------------------------------------------------------------------------------- /src/modules/ui/component/surface/display-input.ts: -------------------------------------------------------------------------------- 1 | import { Component, html, css } from '@readymade/core'; 2 | import { RdControl } from '../control'; 3 | import { RdInput, RdInputAttributes } from '../input/input'; 4 | 5 | @Component({ 6 | selector: 'rd-displayinput', 7 | delegatesFocus: true, 8 | style: css` 9 | :host { 10 | display: inline-block; 11 | outline: none; 12 | padding: 0; 13 | } 14 | :host input { 15 | height: 16px; 16 | width: 36px; 17 | background-color: var(--ready-color-bg); 18 | border: var(--ready-border-width) solid var(--ready-color-border); 19 | border-radius: var(--ready-border-radius); 20 | color: var(--ready-color-default); 21 | font: var(--font-family); 22 | min-height: 2em; 23 | padding: 0em 1em; 24 | } 25 | :host input:hover, 26 | :host input:focus, 27 | :host input:active { 28 | border: var(--ready-border-width) solid var(--ready-color-highlight); 29 | outline: none; 30 | box-shadow: none; 31 | } 32 | :host input[disabled] { 33 | opacity: var(--ready-opacity-disabled); 34 | background: var(--ready-color-disabled); 35 | cursor: not-allowed; 36 | } 37 | :host input[disabled]:hover, 38 | :host input[disabled]:focus, 39 | :host input[disabled]:active { 40 | border: var(--ready-border-width) solid var(--ready-color-border); 41 | outline: none; 42 | box-shadow: none; 43 | } 44 | :host input.required, 45 | :host input.required:hover, 46 | :host input.required:focus, 47 | :host input.required:active { 48 | border: var(--ready-border-width) solid var(--ready-color-error); 49 | outline: none; 50 | box-shadow: none; 51 | } 52 | `, 53 | template: html` `, 54 | }) 55 | class RdDisplayInput extends RdInput { 56 | channel: BroadcastChannel; 57 | control: RdControl; 58 | constructor() { 59 | super(); 60 | } 61 | } 62 | 63 | export { RdDisplayInput }; 64 | -------------------------------------------------------------------------------- /src/modules/ui/component/surface/element.ts: -------------------------------------------------------------------------------- 1 | import { Component, css } from '@readymade/core'; 2 | import { BlockComponent } from '@readymade/dom'; 3 | import { RdControlSurfaceElement } from '../control'; 4 | 5 | @Component({ 6 | selector: 'rd-element', 7 | style: css` 8 | :host { 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | .surface-label { 13 | display: flex; 14 | align-items: center; 15 | margin-bottom: 0.75em; 16 | height: 36px; 17 | } 18 | .surface-label .hint { 19 | display: inline-block; 20 | border-radius: 50%; 21 | width: 20px; 22 | height: 20px; 23 | padding-right: 2px; 24 | background: var(--ready-color-selected); 25 | color: var(--ready-color-default); 26 | font-style: italic; 27 | font-size: 1em; 28 | text-align: center; 29 | margin-left: 0.5em; 30 | cursor: pointer; 31 | user-select: none; 32 | } 33 | [popover] { 34 | position: fixed; 35 | padding: 1em; 36 | border: 1px solid var(--ready-color-border); 37 | border-radius: 0.5em; 38 | background: var(--ready-popover-bg); 39 | color: var(--ready-color-default); 40 | width: 640px; 41 | height: 100vh; 42 | right: 0px; 43 | inset: auto; 44 | overflow: scroll; 45 | } 46 | .rd-display-value { 47 | height: 36px; 48 | &.hidden { 49 | display: none; 50 | } 51 | } 52 | rd-displayinput { 53 | display: inline-block; 54 | margin-left: 10px; 55 | } 56 | ::backdrop { 57 | background: var(--ready-popover-bg); 58 | } 59 | `, 60 | custom: { extends: 'div' }, 61 | }) 62 | class RdSurfaceElement extends BlockComponent { 63 | timeout$: any; 64 | constructor() { 65 | super(); 66 | } 67 | 68 | setControlSurface(surface: RdControlSurfaceElement) { 69 | if (!surface) { 70 | return; 71 | } 72 | if (surface.classes) { 73 | this.setAttribute('class', ''); 74 | surface.classes.forEach((className) => this.classList.add(className)); 75 | } 76 | 77 | if (surface.style) { 78 | for (const styleName in surface.style) { 79 | if (surface.style.hasOwnProperty(styleName)) { 80 | this.style[styleName] = surface.style[styleName]; 81 | } 82 | } 83 | } 84 | 85 | const element = document.createElement(surface.selector); 86 | const label = document.createElement('label'); 87 | label.classList.add('surface-label'); 88 | label.textContent = surface.label; 89 | 90 | if (surface.hint) { 91 | const hint = document.createElement('span'); 92 | hint.classList.add('hint'); 93 | hint.textContent = 'i'; 94 | hint.setAttribute('popovertarget', `${surface.control.name}-dialog`); 95 | label.appendChild(hint); 96 | const dialog = document.createElement('div'); 97 | dialog.setAttribute('id', `${surface.control.name}-dialog`); 98 | dialog.setAttribute('popover', 'auto'); 99 | dialog.innerHTML = surface.hint.template; 100 | 101 | hint.addEventListener('click', () => { 102 | dialog.togglePopover(); 103 | }); 104 | 105 | this.appendChild(dialog); 106 | } 107 | 108 | if (surface.displayValue === true) { 109 | const displayValue = document.createElement('span'); 110 | displayValue.classList.add('rd-display-value'); 111 | label.appendChild(displayValue); 112 | } 113 | 114 | this.appendChild(label); 115 | this.appendChild(element); 116 | 117 | (element as any).setControl(surface.control); 118 | 119 | if (surface.channel) { 120 | (element as any).setChannel(surface.channel); 121 | } 122 | 123 | if (surface.displayValue === true) { 124 | (element as any).onchange = () => { 125 | const displayValue = this.querySelector('.rd-display-value'); 126 | displayValue.classList.remove('hidden'); 127 | window.clearTimeout(this.timeout$); 128 | if (displayValue) { 129 | let inputDebounce: any; 130 | displayValue.innerHTML = ''; 131 | if (Array.isArray((element as any).value)) { 132 | (element as any).value.forEach((value, index) => { 133 | const input = document.createElement( 134 | 'rd-displayinput', 135 | ) as HTMLInputElement; 136 | input.classList.add('rd-display-input'); 137 | input.value = value; 138 | displayValue.appendChild(input); 139 | input.oninput = () => { 140 | window.clearTimeout(this.timeout$); 141 | inputDebounce = setTimeout(() => { 142 | const inputValues = (element as any).value; 143 | if ( 144 | surface.control.currentValue.some( 145 | (val) => typeof val === 'number', 146 | ) 147 | ) { 148 | inputValues[index] = parseFloat(input.value); 149 | } else { 150 | inputValues[index] = input.value; 151 | } 152 | (element as any).value = inputValues; 153 | }, 400); 154 | }; 155 | }); 156 | } else { 157 | const input = document.createElement( 158 | 'rd-displayinput', 159 | ) as HTMLInputElement; 160 | input.classList.add('rd-display-input'); 161 | input.value = (element as any).value; 162 | displayValue.appendChild(input); 163 | input.oninput = () => { 164 | window.clearTimeout(inputDebounce); 165 | inputDebounce = setTimeout(() => { 166 | if (typeof surface.control.currentValue === 'number') { 167 | (element as any).value = parseFloat(input.value); 168 | } else { 169 | (element as any).value = input.value; 170 | } 171 | }, 400); 172 | }; 173 | } 174 | } 175 | this.timeout$ = setTimeout(() => { 176 | displayValue.classList.add('hidden'); 177 | }, 4000); 178 | }; 179 | } 180 | } 181 | } 182 | 183 | export { RdSurfaceElement }; 184 | -------------------------------------------------------------------------------- /src/modules/ui/component/surface/index.ts: -------------------------------------------------------------------------------- 1 | export { RdSurface } from './surface'; 2 | export { RdSurfaceElement } from './element'; 3 | export { RdDisplayInput } from './display-input'; 4 | -------------------------------------------------------------------------------- /src/modules/ui/component/surface/surface.ts: -------------------------------------------------------------------------------- 1 | import { Component, StructuralElement, html, css } from '@readymade/core'; 2 | import { RdControlSurface } from '../control'; 3 | import { RdSurfaceElement } from './element'; 4 | @Component({ 5 | selector: 'rd-surface', 6 | style: css``, 7 | template: html``, 8 | }) 9 | class RdSurface extends StructuralElement { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | setStyle(surface: Partial) { 15 | if (!surface) { 16 | return; 17 | } 18 | if (surface.classes) { 19 | this.setAttribute('class', ''); 20 | surface.classes.forEach((className) => this.classList.add(className)); 21 | } 22 | 23 | if (surface.style) { 24 | for (const styleName in surface.style) { 25 | if (surface.style.hasOwnProperty(styleName)) { 26 | this.style[styleName] = surface.style[styleName]; 27 | } 28 | } 29 | } 30 | } 31 | 32 | setControlSurface(surface: Partial) { 33 | if (!surface) { 34 | return; 35 | } 36 | 37 | this.setStyle(surface); 38 | 39 | for (let i = 0; i <= surface.controls.length; i++) { 40 | const element = (( 41 | document.createElement('div', { is: 'rd-element' }) 42 | )) as RdSurfaceElement; 43 | 44 | element.setAttribute('is', 'rd-element'); 45 | element.setControlSurface(surface.controls[i]); 46 | this.appendChild(element); 47 | } 48 | } 49 | } 50 | 51 | export { RdSurface }; 52 | -------------------------------------------------------------------------------- /src/modules/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | RdButton, 3 | RdButtonPad, 4 | StandardKeyboard, 5 | StandardKeyboardModifiers, 6 | StandardKeyboardNumPad, 7 | StandardKeyboardModifierCodeKeyMap, 8 | RdSwitch, 9 | RdInput, 10 | RdRadioGroup, 11 | RdCheckBox, 12 | RdTextArea, 13 | RdDropdown, 14 | RdSlider, 15 | RdDial, 16 | RdSurface, 17 | RdSurfaceElement, 18 | RdDisplayInput, 19 | } from './component'; 20 | export type { 21 | RdLegacyControl, 22 | RdControl, 23 | RdControlSurface, 24 | RdControlSurfaceElement, 25 | } from './component/control'; 26 | -------------------------------------------------------------------------------- /src/modules/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@readymade/ui", 3 | "version": "3.1.3", 4 | "description": "UI library of standard elements built with Readymade", 5 | "type": "module", 6 | "module": "./fesm2022/index.js", 7 | "typings": "./typings/ui/index.d.ts", 8 | "exports": { 9 | "./package.json": { 10 | "default": "./package.json" 11 | }, 12 | ".": { 13 | "types": "./typings/ui/index.d.ts", 14 | "esm": "./esm2022/ui/index.js", 15 | "esm2022": "./esm2022/ui/index.js", 16 | "default": "./fesm2022/index.js" 17 | } 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readymade-ui/readymade.git" 25 | }, 26 | "keywords": [ 27 | "custom elements", 28 | "web components", 29 | "typescript", 30 | "decorators", 31 | "javascript" 32 | ], 33 | "author": "Stephen Belovarich", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/readymade-ui/readymade/issues" 37 | }, 38 | "homepage": "https://github.com/readymade-ui/readymade#readme" 39 | } -------------------------------------------------------------------------------- /src/modules/ui/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import pkgMinifyHTML from 'rollup-plugin-minify-html-literals'; 4 | import pkgInlinePostCSS from 'rollup-plugin-inline-postcss'; 5 | import cleanup from 'rollup-plugin-cleanup'; 6 | import terser from '@rollup/plugin-terser'; 7 | 8 | const minifyHTML = pkgMinifyHTML.default; 9 | const inlinePostCSS = pkgInlinePostCSS.default; 10 | 11 | const clean = { 12 | comments: ['none'], 13 | extensions: ['ts', 'js'], 14 | }; 15 | 16 | export default [ 17 | { 18 | input: 'src/modules/ui/index.ts', 19 | plugins: [ 20 | resolve(), 21 | typescript({ 22 | sourceMap: false, 23 | declarationDir: 'dist/packages/@readymade/ui/fesm2022/typings', 24 | }), 25 | cleanup(clean), 26 | ], 27 | onwarn: (warning, next) => { 28 | if (warning.code === 'THIS_IS_UNDEFINED') return; 29 | next(warning); 30 | }, 31 | output: { 32 | file: 'dist/packages/@readymade/ui/fesm2022/index.js', 33 | format: 'esm', 34 | sourcemap: true, 35 | }, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /src/modules/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "typings" 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/server/config.ts: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development'; 2 | 3 | interface ReadymadeEnvironmentConfig { 4 | env: string; 5 | host: string; 6 | protocol: string; 7 | port: string; 8 | hmrPort?: string; 9 | ignoreHTMLMinify?: Set; 10 | } 11 | 12 | let config: ReadymadeEnvironmentConfig; 13 | 14 | if (env === 'development') { 15 | config = { 16 | env: 'development', 17 | host: 'http://localhost:4443', 18 | protocol: 'http', 19 | port: '4443', 20 | hmrPort: '7443', 21 | }; 22 | } 23 | 24 | if (env === 'production') { 25 | config = { 26 | env: 'production', 27 | host: 'http://localhost:4444', 28 | protocol: 'http', 29 | port: '4444', 30 | ignoreHTMLMinify: new Set(['home']), 31 | }; 32 | } 33 | 34 | export { config, ReadymadeEnvironmentConfig }; 35 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server.js'; 2 | -------------------------------------------------------------------------------- /src/server/middleware/ssr.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import he from 'he'; 4 | import { html } from 'lit'; 5 | import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; 6 | import { render } from '@lit-labs/ssr'; 7 | import { Readable } from 'stream'; 8 | import { minify } from 'html-minifier-terser'; 9 | import { ViteDevServer } from 'vite'; 10 | 11 | import { config } from '../config.js'; 12 | 13 | import * as cheerio from 'cheerio'; 14 | 15 | interface View { 16 | render: () => string; 17 | } 18 | 19 | const SSR_OUTLET_MARKER = ''; 20 | const GLOBAL_STYLE_MARKER = ''; 21 | 22 | async function* concatStreams(...readables) { 23 | for (const readable of readables) { 24 | for await (const chunk of readable) { 25 | yield chunk; 26 | } 27 | } 28 | } 29 | 30 | export const sanitizeTemplate = async (template) => { 31 | return html`${unsafeHTML(template)}`; 32 | }; 33 | 34 | async function streamToString(stream) { 35 | const chunks = []; 36 | for await (const chunk of stream) { 37 | chunks.push(Buffer.from(chunk)); 38 | } 39 | return Buffer.concat(chunks).toString('utf-8'); 40 | } 41 | 42 | async function renderStream(stream) { 43 | return await streamToString(Readable.from(stream)); 44 | } 45 | 46 | async function* renderView(template) { 47 | yield* render(template); 48 | } 49 | 50 | function isRoute(req): boolean { 51 | const baseUrl = req.baseUrl.split('/')[1]; // Get the first segment of the baseUrl 52 | const isRouteSegment = !/\.[^/.]+$/.test(baseUrl); // Check if it does not contain a file extension 53 | return isRouteSegment; 54 | } 55 | 56 | function readFilesSync(filePaths) { 57 | const results = filePaths.map((path) => { 58 | const content = fs.readFileSync(path, 'utf-8'); 59 | return content; 60 | }); 61 | return results; 62 | } 63 | 64 | const ssrMiddleware = (options?: { vite?: ViteDevServer }) => { 65 | return async (req, res, next) => { 66 | let routeDirectoryName: string; 67 | 68 | if (req.baseUrl === '/home') { 69 | return res.redirect('/'); 70 | } 71 | 72 | if (!isRoute(req)) { 73 | next(); 74 | } 75 | 76 | if (!req.baseUrl.length) { 77 | routeDirectoryName = 'home'; 78 | } else { 79 | routeDirectoryName = req.baseUrl.split('/')[1]; 80 | } 81 | 82 | const url = req.originalUrl; 83 | const vite = options.vite; 84 | const env: string = process.env.NODE_ENV || 'development'; 85 | const root = process.cwd(); 86 | const resolve = (p) => path.resolve(root, p); 87 | 88 | try { 89 | let view: View = { 90 | render: () => '', 91 | }; 92 | let template: string, filePath: string, routeTemplateFilePath: string; 93 | let stylesheets; 94 | 95 | if (env === 'development') { 96 | template = fs.readFileSync(resolve('src/client/index.html'), 'utf-8'); 97 | template = await vite.transformIndexHtml(url, template); 98 | filePath = `app/view/${routeDirectoryName}/index.ts`; 99 | routeTemplateFilePath = resolve(`src/client/${filePath}`); 100 | view = (await vite.ssrLoadModule(routeTemplateFilePath)) as View; 101 | } else { 102 | template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8'); 103 | const manifest = JSON.parse( 104 | fs.readFileSync(resolve('dist/client/manifest.json'), 'utf-8'), 105 | ); 106 | const indexManifest = JSON.parse( 107 | fs.readFileSync(resolve('dist/client/manifest-index.json'), 'utf-8'), 108 | ); 109 | if (manifest && manifest[`app/view/${routeDirectoryName}/index.ts`]) { 110 | filePath = manifest[`app/view/${routeDirectoryName}/index.ts`].file; 111 | routeTemplateFilePath = resolve(`dist/client/${filePath}`); 112 | view = (await import(routeTemplateFilePath)) as View; 113 | } else { 114 | filePath = manifest[`app/view/404/index.ts`].file; 115 | routeTemplateFilePath = resolve(`dist/client/${filePath}`); 116 | view = (await import(routeTemplateFilePath)) as View; 117 | } 118 | if (indexManifest && indexManifest[`index.html`].css) { 119 | stylesheets = readFilesSync( 120 | indexManifest[`index.html`].css.map((path) => 121 | resolve(`dist/client/${path}`), 122 | ), 123 | ) 124 | .map((stylesheet) => ``) 125 | .join('\n') 126 | .trim() 127 | .concat('\n'); 128 | } 129 | } 130 | 131 | // if you need to modify the index template here 132 | const $ = cheerio.load(template); 133 | template = $.html(); 134 | 135 | if (env === 'production') { 136 | $('[rel="stylesheet"]').remove(); 137 | template = $.html(); 138 | const styleIndex = template.indexOf(GLOBAL_STYLE_MARKER); 139 | const preStyle = Readable.from(template.substring(0, styleIndex)); 140 | const postStyle = Readable.from( 141 | template.substring(styleIndex + GLOBAL_STYLE_MARKER.length + 1), 142 | ); 143 | const styleResult = Readable.from(stylesheets); 144 | template = await renderStream( 145 | Readable.from(concatStreams(preStyle, styleResult, postStyle)), 146 | ); 147 | } 148 | 149 | const ssrIndex = template.indexOf(SSR_OUTLET_MARKER); 150 | const preSSR = Readable.from(template.substring(0, ssrIndex)); 151 | const postSSR = Readable.from( 152 | template.substring(ssrIndex + SSR_OUTLET_MARKER.length + 1), 153 | ); 154 | const viewTemplate = view.render(); 155 | const ssrResult = await renderView(viewTemplate); 156 | const viewResult = await renderStream(ssrResult); 157 | 158 | const output = await renderStream( 159 | Readable.from(concatStreams(preSSR, viewResult, postSSR)), 160 | ); 161 | 162 | if (env === 'production') { 163 | if (config.ignoreHTMLMinify?.has(routeDirectoryName)) { 164 | res.status(200).send(he.decode(output)); 165 | } else { 166 | minify(he.decode(output), { 167 | minifyCSS: true, 168 | removeComments: true, 169 | collapseWhitespace: true, 170 | conservativeCollapse: true, 171 | }).then((html) => { 172 | res.status(200).send(html); 173 | }); 174 | } 175 | } else { 176 | res 177 | .status(200) 178 | .set({ 'Content-Type': 'text/html' }) 179 | .send(he.decode(output)); 180 | } 181 | } catch (e) { 182 | console.log(e.stack); 183 | res.status(500).end(e.stack); 184 | } 185 | }; 186 | }; 187 | 188 | export { ssrMiddleware }; 189 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { installShimOnGlobal } from './shim.js'; 2 | 3 | installShimOnGlobal(); 4 | 5 | import path from 'path'; 6 | import chalk from 'chalk'; 7 | import express from 'express'; 8 | import helmet from 'helmet'; 9 | import cors from 'cors'; 10 | import { UserConfig } from 'vite'; 11 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 12 | 13 | import { config } from './config.js'; 14 | 15 | import { ssrMiddleware } from './middleware/ssr.js'; 16 | 17 | const env: string = process.env.NODE_ENV || 'development'; 18 | const port: string = process.env.PORT || config.port || '4443'; 19 | const hmrPort: string = process.env.HMR_PORT || config.hmrPort || '7443'; 20 | 21 | // import { fileURLToPath } from 'url'; 22 | // const __dirname = path.dirname(fileURLToPath(import.meta.url)); 23 | 24 | async function createServer(root = process.cwd()) { 25 | const resolve = (p) => path.resolve(root, p); 26 | const app: express.Application = express(); 27 | 28 | const corsOptions = 29 | env === 'production' 30 | ? { origin: `${config.protocol}://${config.host}` } 31 | : {}; 32 | 33 | const hmrOptions = 34 | env === 'development' 35 | ? { connectSrc: ["'self'", `ws://localhost:${hmrPort}`] } 36 | : {}; 37 | 38 | const helmetConfig = { 39 | crossOriginEmbedderPolicy: false, 40 | contentSecurityPolicy: { 41 | directives: { 42 | defaultSrc: ["'self'"], 43 | fontSrc: ["'self'", 'https://fonts.gstatic.com'], 44 | scriptSrc: [ 45 | "'self'", 46 | () => `'sha256-5+YTmTcBwCYdJ8Jetbr6kyjGp0Ry/H7ptpoun6CrSwQ='`, 47 | ], 48 | ...hmrOptions, 49 | }, 50 | }, 51 | }; 52 | 53 | app.use(helmet(helmetConfig)); 54 | app.use(cors(corsOptions)); 55 | 56 | if (env === 'production') { 57 | app.use((await import('compression')).default()); 58 | app.use( 59 | (await import('serve-static')).default(resolve('dist/client'), { 60 | index: false, 61 | }), 62 | ); 63 | app.use('*', ssrMiddleware({})); 64 | } else { 65 | const viteServerConfig: UserConfig = { 66 | base: resolve('src/client/'), 67 | root: resolve('src/client'), 68 | appType: 'custom', 69 | server: { 70 | middlewareMode: true, 71 | port: Number(port), 72 | hmr: { 73 | protocol: 'ws', 74 | port: Number(hmrPort), 75 | }, 76 | }, 77 | plugins: [tsconfigPaths()], 78 | }; 79 | const vite = await ( 80 | await import('vite') 81 | ).createServer((viteServerConfig) as UserConfig); 82 | app.use(vite.middlewares); 83 | app.use('*', ssrMiddleware({ vite })); 84 | } 85 | 86 | return { app }; 87 | } 88 | 89 | createServer().then(({ app }) => { 90 | const port: string = process.env.PORT || config.port || '4443'; 91 | app.listen(port, (): void => { 92 | const addr = `${ 93 | config.protocol === 'HTTPS' ? 'https' : 'http' 94 | }://localhost:${port}`; 95 | process.stdout.write( 96 | `\n [${new Date().toISOString()}] ${chalk.green( 97 | 'Server running:', 98 | )} ${chalk.blue(addr)} \n`, 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/server/shim.ts: -------------------------------------------------------------------------------- 1 | import { installWindowOnGlobal } from '@lit-labs/ssr/lib/dom-shim.js'; 2 | 3 | const attributes = new WeakMap(); 4 | const attributesForElement = (element) => { 5 | let attrs = attributes.get(element); 6 | if (!attrs) { 7 | attributes.set(element, (attrs = new Map())); 8 | } 9 | return attrs; 10 | }; 11 | class Element {} 12 | class HTMLElement extends Element { 13 | get attributes() { 14 | return Array.from(attributesForElement(this)).map(([name, value]) => ({ 15 | name, 16 | value, 17 | })); 18 | } 19 | setAttribute(name, value) { 20 | attributesForElement(this).set(name, value); 21 | } 22 | removeAttribute(name) { 23 | attributesForElement(this).delete(name); 24 | } 25 | hasAttribute(name) { 26 | return attributesForElement(this).has(name); 27 | } 28 | attachShadow() { 29 | return { host: this }; 30 | } 31 | getAttribute(name) { 32 | const value = attributesForElement(this).get(name); 33 | return value === undefined ? null : value; 34 | } 35 | } 36 | class HTMLAnchorElement extends HTMLElement {} 37 | class HTMLAreaElement extends HTMLElement {} 38 | class HTMLAllCollection extends HTMLElement {} 39 | class HTMLCollection extends HTMLElement {} 40 | class HTMLAudioElement extends HTMLElement {} 41 | class HTMLBaseElement extends HTMLElement {} 42 | class HTMLBodyElement extends HTMLElement {} 43 | class HTMLBRElement extends HTMLElement {} 44 | class HTMLButtonElement extends HTMLElement {} 45 | class HTMLDListElement extends HTMLElement {} 46 | class HTMLCanvasElement extends HTMLElement {} 47 | class HTMLDataElement extends HTMLElement {} 48 | class HTMLDataListElement extends HTMLElement {} 49 | class HTMLDetailsElement extends HTMLElement {} 50 | class HTMLDialogElement extends HTMLElement {} 51 | class HTMLDivElement extends HTMLElement {} 52 | class HTMLEmbedElement extends HTMLElement {} 53 | class HTMLFieldSetElement extends HTMLElement {} 54 | class HTMLFormElement extends HTMLElement {} 55 | class HTMLFormControlsCollection extends HTMLElement {} 56 | class HTMLHeadingElement extends HTMLElement {} 57 | class HTMLHeadElement extends HTMLElement {} 58 | class HTMLHRElement extends HTMLElement {} 59 | class HTMLHtmlElement extends HTMLElement {} 60 | class HTMLIFrameElement extends HTMLElement {} 61 | class HTMLImageElement extends HTMLElement {} 62 | class HTMLInputElement extends HTMLElement {} 63 | class HTMLLabelElement extends HTMLElement {} 64 | class HTMLLegendElement extends HTMLElement {} 65 | class HTMLLIElement extends HTMLElement {} 66 | class HTMLLinkElement extends HTMLElement {} 67 | class HTMLMapElement extends HTMLElement {} 68 | class HTMLMediaElement extends HTMLElement {} 69 | class HTMLMenuElement extends HTMLElement {} 70 | class HTMLMetaElement extends HTMLElement {} 71 | class HTMLMeterElement extends HTMLElement {} 72 | class HTMLModElement extends HTMLElement {} 73 | class HTMLOListElement extends HTMLElement {} 74 | class HTMLObjectElement extends HTMLElement {} 75 | class HTMLOptGroupElement extends HTMLElement {} 76 | class HTMLOptionElement extends HTMLElement {} 77 | class HTMLOptionsCollection extends HTMLElement {} 78 | class HTMLOutputElement extends HTMLElement {} 79 | class HTMLParagraphElement extends HTMLElement {} 80 | class HTMLParamElement extends HTMLElement {} 81 | class HTMLPictureElement extends HTMLElement {} 82 | class HTMLPreElement extends HTMLElement {} 83 | class HTMLProgressElement extends HTMLElement {} 84 | class HTMLQuoteElement extends HTMLElement {} 85 | class HTMLScriptElement extends HTMLElement {} 86 | class HTMLSelectElement extends HTMLElement {} 87 | class HTMLSlotElement extends HTMLElement {} 88 | class HTMLSourceElement extends HTMLElement {} 89 | class HTMLSpanElement extends HTMLElement {} 90 | class HTMLStyleElement extends HTMLElement {} 91 | class HTMLTableElement extends HTMLElement {} 92 | class HTMLTableCaptionElement extends HTMLElement {} 93 | class HTMLTableCellElement extends HTMLElement {} 94 | class HTMLTableColElement extends HTMLElement {} 95 | class HTMLTableRowElement extends HTMLElement {} 96 | class HTMLTableSectionElement extends HTMLElement {} 97 | class HTMLTemplateElement extends HTMLElement {} 98 | class HTMLTextAreaElement extends HTMLElement {} 99 | class HTMLTimeElement extends HTMLElement {} 100 | class HTMLTitleElement extends HTMLElement {} 101 | class HTMLTrackElement extends HTMLElement {} 102 | class HTMLUListElement extends HTMLElement {} 103 | class HTMLUnknownElement extends HTMLElement {} 104 | class HTMLVideoElement extends HTMLElement {} 105 | 106 | const installShimOnGlobal = (props = {}) => { 107 | installWindowOnGlobal({ 108 | HTMLAnchorElement, 109 | HTMLAllCollection, 110 | HTMLCollection, 111 | HTMLAreaElement, 112 | HTMLAudioElement, 113 | HTMLBaseElement, 114 | HTMLBodyElement, 115 | HTMLBRElement, 116 | HTMLButtonElement, 117 | HTMLCanvasElement, 118 | HTMLDataElement, 119 | HTMLDataListElement, 120 | HTMLDetailsElement, 121 | HTMLDialogElement, 122 | HTMLDivElement, 123 | HTMLDListElement, 124 | HTMLEmbedElement, 125 | HTMLFormControlsCollection, 126 | HTMLFieldSetElement, 127 | HTMLFormElement, 128 | HTMLHeadingElement, 129 | HTMLHeadElement, 130 | HTMLHRElement, 131 | HTMLHtmlElement, 132 | HTMLIFrameElement, 133 | HTMLImageElement, 134 | HTMLInputElement, 135 | HTMLLabelElement, 136 | HTMLLegendElement, 137 | HTMLLIElement, 138 | HTMLLinkElement, 139 | HTMLMapElement, 140 | HTMLMediaElement, 141 | HTMLMenuElement, 142 | HTMLMetaElement, 143 | HTMLMeterElement, 144 | HTMLModElement, 145 | HTMLOListElement, 146 | HTMLObjectElement, 147 | HTMLOptGroupElement, 148 | HTMLOptionElement, 149 | HTMLOptionsCollection, 150 | HTMLOutputElement, 151 | HTMLParamElement, 152 | HTMLParagraphElement, 153 | HTMLPictureElement, 154 | HTMLPreElement, 155 | HTMLProgressElement, 156 | HTMLQuoteElement, 157 | HTMLScriptElement, 158 | HTMLSelectElement, 159 | HTMLSlotElement, 160 | HTMLSourceElement, 161 | HTMLSpanElement, 162 | HTMLStyleElement, 163 | HTMLTableElement, 164 | HTMLTableCaptionElement, 165 | HTMLTableCellElement, 166 | HTMLTableColElement, 167 | HTMLTableRowElement, 168 | HTMLTableSectionElement, 169 | HTMLTemplateElement, 170 | HTMLTextAreaElement, 171 | HTMLTimeElement, 172 | HTMLTitleElement, 173 | HTMLTrackElement, 174 | HTMLUListElement, 175 | HTMLUnknownElement, 176 | HTMLVideoElement, 177 | ...props, 178 | }); 179 | }; 180 | installShimOnGlobal(); 181 | export { installShimOnGlobal }; 182 | -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "declaration": true, 5 | "target": "ES2022", 6 | "useDefineForClassFields": false, 7 | "module": "ES2022", 8 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "moduleResolution": "node", 13 | "isolatedModules": false, 14 | "allowSyntheticDefaultImports": true, 15 | "paths": { 16 | "@readymade/core": ["src/modules/core"], 17 | "@readymade/dom": ["src/modules/dom"], 18 | "@readymade/router": ["src/modules/router"], 19 | "@readymade/ui": ["src/modules/ui"], 20 | "@readymade/transmit": ["src/modules/transmit"] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css?raw' { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module '*.html?raw' { 7 | const content: string; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.hello.js: -------------------------------------------------------------------------------- 1 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 2 | 3 | export default { 4 | plugins: [tsconfigPaths()], 5 | build: { 6 | minify: true, 7 | rollupOptions: { 8 | input: 'src/client/hello.ts', 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /vite.config.index.js: -------------------------------------------------------------------------------- 1 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 2 | // import { viteStaticCopy } from 'vite-plugin-static-copy'; 3 | 4 | export default { 5 | esbuild: { 6 | format: 'esm', 7 | target: 'es2022', 8 | }, 9 | plugins: [ 10 | tsconfigPaths(), 11 | // viteStaticCopy({ 12 | // targets: [ 13 | // { 14 | // src: 'public/images', 15 | // dest: 'images', 16 | // }, 17 | // ], 18 | // }), 19 | { 20 | name: 'remove-type-module', 21 | transformIndexHtml(html) { 22 | return html.replace( 23 | /', 25 | ); 26 | }, 27 | }, 28 | ], 29 | build: { 30 | minify: false, 31 | manifest: 'manifest-index.json', 32 | rollupOptions: { 33 | output: { 34 | format: 'esm', 35 | sourcemap: false, 36 | extend: true, 37 | }, 38 | plugins: [], 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /vite.config.inline.js: -------------------------------------------------------------------------------- 1 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 2 | import { viteSingleFile } from 'vite-plugin-singlefile'; 3 | export default { 4 | plugins: [ 5 | tsconfigPaths(), 6 | { 7 | name: 'remove-type-module', 8 | transformIndexHtml(html) { 9 | return html.replace( 10 | /' 12 | ); 13 | }, 14 | }, 15 | viteSingleFile(), 16 | ], 17 | esbuild: { 18 | format: 'esm', 19 | target: 'es2022', 20 | }, 21 | rollupOptions: { 22 | output: { 23 | name: 'window', 24 | sourcemap: false, 25 | extend: true, 26 | }, 27 | }, 28 | build: { 29 | minify: false, 30 | rollupOptions: { 31 | output: { 32 | name: 'window', 33 | sourcemap: false, 34 | extend: true, 35 | }, 36 | plugins: [], 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 2 | // import { viteStaticCopy } from 'vite-plugin-static-copy'; 3 | 4 | export default { 5 | plugins: [ 6 | tsconfigPaths(), 7 | // viteStaticCopy({ 8 | // targets: [ 9 | // { 10 | // src: 'public/images', 11 | // dest: 'images', 12 | // }, 13 | // ], 14 | // }), 15 | { 16 | name: 'remove-type-module', 17 | transformIndexHtml(html) { 18 | return html.replace( 19 | /', 21 | ); 22 | }, 23 | }, 24 | ], 25 | esbuild: { 26 | format: 'esm', 27 | target: 'es2022', 28 | }, 29 | rollupOptions: { 30 | output: { 31 | name: 'window', 32 | sourcemap: false, 33 | extend: true, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /vite.config.routes.js: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob'; 2 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 3 | 4 | export default { 5 | plugins: [tsconfigPaths()], 6 | esbuild: { 7 | format: 'esm', 8 | target: 'es2022', 9 | }, 10 | build: { 11 | ssr: true, 12 | minify: false, 13 | manifest: 'manifest.json', 14 | rollupOptions: { 15 | input: await glob(['src/client/app/view/**/index.ts']), 16 | output: { 17 | name: 'window', 18 | format: 'esm', 19 | sourcemap: false, 20 | extend: true, 21 | }, 22 | plugins: [], 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /vite.config.server.js: -------------------------------------------------------------------------------- 1 | import { tsconfigPaths } from 'vite-resolve-tsconfig-paths'; 2 | 3 | export default { 4 | plugins: [tsconfigPaths()], 5 | build: { 6 | minify: true, 7 | rollupOptions: { 8 | external: ['crypto'], 9 | }, 10 | }, 11 | }; 12 | --------------------------------------------------------------------------------