├── .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 |
87 |
90 | ${copy}
91 |
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 |
63 |
66 | ${renderHeadline({ size, copy: _logoState.heading })}
67 | ${renderHeadline({ size: 'is--default', copy: _logoState.heading2 })}
68 |
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 |
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 |
36 |
39 | ${template}
40 |
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 |
20 |
23 | ${template}
24 |
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 |
31 |
34 | ${template}
35 |
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 |
32 | - {{item.title}}
33 |
34 |
35 |
36 |
37 |
38 | - {{sub}}
39 |
40 |
41 |
42 |
43 |
44 |
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 |
54 |
57 |
58 |
59 | ${renderLogo({ size: 'is--large' })}
60 |
61 |
62 | -
63 | Item 1
64 |
65 | -
66 | Item 2
67 |
68 | -
69 | Item 3
70 |
71 | -
72 | Item 4
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | - {{item.title}}
89 |
90 |
91 |
92 |
93 |
94 | - {{sub}}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
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 |
20 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
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 |
--------------------------------------------------------------------------------