Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7d6d650bc | |||
| 53c5d839ca | |||
| 6cbfd714eb | |||
| 7c8c194fd8 | |||
| 278000bb36 | |||
| 52ffe81352 | |||
| 91194f6388 | |||
| 904bb92057 | |||
| 4c23739d9a | |||
| dd048d42a8 | |||
| ca28dbd9db | |||
| 7148b12066 | |||
| 309d708830 | |||
| 923bedc4fc | |||
| e8b771bde4 | |||
| 7a248993bc | |||
| 03f215e0f1 | |||
| 216cb0288d | |||
| 65acda3de1 | |||
| 88ff74bb86 | |||
| 98a5b1b5a3 | |||
| bbf738d4e2 | |||
| 4f8ca7061a | |||
| d26d99dbff | |||
| c1d8e347de | |||
| b6c41caf44 | |||
| b858b3b9e2 | |||
| 4ed37086ae | |||
| b4c0de47b9 | |||
| e11f0df950 | |||
| c64b106569 | |||
| 3d1948b93e | |||
| 21b7158a35 | |||
| 992af2668e | |||
| 0cd84b28b4 | |||
| 0709267bd5 | |||
| d4fce8a939 | |||
| 578f87a8f9 | |||
| 0acf341071 | |||
| 87bbf0bbdc | |||
| bed41da573 | |||
| 48f77e7ba2 | |||
| f7bc113b6c | |||
| 05934132a7 | |||
| 6bdb8c78b7 | |||
| fca47b87fb | |||
| 595620af4b | |||
| 8c3a116943 | |||
| 269b90c64c | |||
| 6d74bdfb51 | |||
| 6aa1e9bc2a | |||
| 3f83bb9fba | |||
| 57bbca6b28 | |||
| 51382611cf | |||
| 3094c0b815 | |||
| f5273b6b82 | |||
| 9c51d93418 | |||
| 5a769ef7e2 | |||
| 8f71d1afde | |||
| 6f7dace5da | |||
| 4008a5ab62 | |||
| 13f1d4698f | |||
| d93e183db1 | |||
| bff3b1f567 | |||
| 39fb873aec | |||
| a2f2605241 | |||
| 8dfb876988 | |||
| 5db7fc9a3b | |||
| 690b85f057 | |||
| eea091cb56 | |||
| 8d725ef303 | |||
| 4aa2708f24 | |||
| d1848f31a7 | |||
| 5aba0a7fa5 | |||
| 134774b870 | |||
| 43897f0fb5 | |||
| 2e57176dcc | |||
| d1fe66f1ba | |||
| 8ea7c53154 | |||
| ea0858fd27 | |||
| 03c00919df | |||
| 6917145b58 | |||
| 2e2ad98ed8 | |||
| 68375a5e58 | |||
| 5876225b39 | |||
| 342ac96429 | |||
| 85a472fe1c | |||
| 4fecae83dc | |||
| b7a666ac66 | |||
| e559ed072c | |||
| c44d9e7365 | |||
| 53f53be991 | |||
| 9dec6e25b5 | |||
| 03fa323eb9 | |||
| e0344bb513 | |||
| a6f583b8c4 | |||
| 60e173b7be | |||
| d51027d4be | |||
| 2efa465930 | |||
| 19081caacd | |||
| c42e8b8bef | |||
| 750e8cef32 | |||
| c0d057949d | |||
| 82a5283edd | |||
| abb20def4b | |||
| dd71751864 | |||
| c4562c797e | |||
| 643317811b | |||
| 3667070094 | |||
| a86ef5bfce | |||
| 77d058f403 | |||
| 1639a57bd1 | |||
| b546d1a2c5 | |||
| 5639c152a2 | |||
| 05a093f080 | |||
| 08156b2d47 | |||
| 0b119c481c | |||
| bf4ef900f8 | |||
| a812a12c10 | |||
| 71e885f3e4 | |||
| 57bc2b76bc | |||
| f21a20b652 | |||
| 91b9c424d8 | |||
| 5cf4752ad9 | |||
| 5f347153fc | |||
| e1aebc7db8 | |||
| 97b88d965c | |||
| 37d343da03 | |||
| 47cb726716 | |||
| 4220131bc4 | |||
| edee4fa446 | |||
| c2a0fd26e2 | |||
| 693c8ca3f0 | |||
| ca58c55a37 | |||
| 79d2be98c5 | |||
| 7971f48963 | |||
| a15629a960 | |||
| ad5c25d80e | |||
| a713d88f27 | |||
| 100f657e04 | |||
| 46e0b9f9db | |||
| 64d6379cd2 | |||
| 847733286c | |||
| 67a037c511 | |||
| 21bcdb2e01 | |||
| 1743490a26 | |||
| 2f9ec6a700 | |||
| 3a89888289 | |||
| 98909c61ff | |||
| 324808dd03 | |||
| c5eec2ac22 | |||
| b1350c463b | |||
| 40f54c574d | |||
| ebc2c82b7f | |||
| 15481c5e24 | |||
| 357320d40a | |||
| 08d92d9d3e | |||
| 0ce9a96b90 | |||
| 959a4cae89 | |||
| c8d6986aff | |||
| 53ff02c490 | |||
| ba83aad026 | |||
| 2531126935 | |||
| 00a2de6560 | |||
| b357bc67fa | |||
| 0665f85d49 | |||
| c222a66ead |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
127
.gitlab-ci.yml
127
.gitlab-ci.yml
@@ -1,127 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
mirror:
|
||||
stage: security
|
||||
script:
|
||||
- npmci git mirror
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
audit:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install --ignore-scripts
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command npm audit --audit-level=high
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
script:
|
||||
- npmci command npm install -g tslint typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install lts
|
||||
- npmci command npm install -g @gitzone/tsdoc
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command tsdoc
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
||||
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
@@ -2,28 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "current file",
|
||||
"type": "node",
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "test.ts",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"test/test.ts"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -15,7 +15,7 @@
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm"]
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
changelog.md
Normal file
86
changelog.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-11 - 1.3.0 - feat(recording-panel)
|
||||
Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation
|
||||
|
||||
- Add dees-demowrapper (ts_demotools) with runAfterRender callback to run post-render demo logic (supports async callbacks).
|
||||
- Improve recording UI and trimming: handle WebM files with Infinity/NaN durations by falling back to tracked recording duration; replace numeric handle positioning with CSS calc strings for responsive trim handles.
|
||||
- Harden property extraction: implement recursive element search (including shadowRoots), add an initial delay and retry loop to wait for demo rendering, and add an advanced JSON editor for Object/Array properties with open/save/cancel and per-editor error reporting.
|
||||
- Add and expand documentation: new ts_web/ and ts_demotools/ READMEs, reorganized main README with clearer feature list, usage examples, and API reference.
|
||||
- Minor exports and module/docs housekeeping (index exports, readme reorder, examples updated to import classes).
|
||||
|
||||
## 2025-11-16 - 1.2.1 - fix(dependencies)
|
||||
Bump dependencies and developer tooling versions
|
||||
|
||||
- Bump @design.estate/dees-domtools from ^2.3.3 to ^2.3.6
|
||||
- Bump @design.estate/dees-element from ^2.1.2 to ^2.1.3
|
||||
- Upgrade @git.zone/tsbuild from ^2.6.8 to ^2.7.1
|
||||
- Upgrade @git.zone/tsrun from ^1.2.44 to ^1.6.2
|
||||
- Upgrade @git.zone/tstest from ^2.3.8 to ^2.7.0
|
||||
|
||||
## 2025-09-19 - 1.2.0 - feat(wcc-properties)
|
||||
Add advanced property editors, recursive element detection, demo wrapper, UI refresh and test fixtures
|
||||
|
||||
- Advanced JSON property editor: multiple side-by-side editors with save/cancel, syntax validation and inline error display; editors affect frame layout (frame bottom increases when editors open).
|
||||
- Improved properties panel element detection: recursive search through nested children and shadow DOM, initial delay and retry mechanism to handle async Lit rendering.
|
||||
- Add dees-demowrapper component in ts_demotools to run post-render callbacks and support async demo setup and DOM access for demos.
|
||||
- UI refresh with shadcn-like styles: CSS variables for theming, redesigned properties panel and sidebar, improved form controls, theme and viewport selectors.
|
||||
- Viewport and frame improvements: responsive padding based on viewport type, theme-aware background rendering, and scroll position tracking with URL/state restoration for frame and sidebar.
|
||||
- Add test fixtures and demo elements/pages under test/ to exercise properties, complex types, nested elements and scroll restoration; include node test for resolveTemplateFactory.
|
||||
- Expose setupWccTools entry point and plugin wiring (wcctools.plugins exports for dees-domtools and smartdelay) for easier integration.
|
||||
|
||||
## 2025-06-27 - 1.1.0 - feat(wcctools)
|
||||
Enhance component tools with an advanced property editor, improved element detection and modernized UI styling for a more responsive dashboard experience.
|
||||
|
||||
- Updated documentation and in-code hints with new shadcn-like design patterns for the dashboard UI.
|
||||
- Introduced an advanced complex properties editor supporting JSON validation and multi-editor handling.
|
||||
- Refined recursive element search in the properties panel to improve asynchronous rendering detection.
|
||||
- Expanded test coverage with scenarios for edge cases, nested elements and wrapper components.
|
||||
|
||||
## 2025-06-26 - 1.0.101 - fix(wcc-dashboard)
|
||||
Improve scroll listener management and add new test pages
|
||||
|
||||
- Removed the pages/ directory entry from .gitignore to allow test pages to be tracked
|
||||
- Added new test pages: page1 and pageLongScroll for enhanced scroll and navigation testing
|
||||
- Refactored wcc-dashboard: changed scroll position properties to private variables and added a flag to prevent duplicate scroll listener attachment
|
||||
|
||||
## 2025-06-26 - 1.0.100 - fix(wcc-dashboard)
|
||||
Prevent duplicate application of scroll positions in dashboard to avoid interfering with user scrolling
|
||||
|
||||
- Added a private 'scrollPositionsApplied' property to track if scroll positions have already been applied
|
||||
- Introduced a guard in the applyScrollPositions method to ensure the scroll state is applied only once
|
||||
|
||||
## 2025-06-26 - 1.0.99 - fix(dashboard)
|
||||
Fix scroll state preservation in dashboard by tracking frame and sidebar scroll positions and updating the URL accordingly.
|
||||
|
||||
- Added frameScrollY and sidebarScrollY properties to capture scroll positions.
|
||||
- Set up scroll listeners on wcc-frame and wcc-sidebar to update scroll state.
|
||||
- Implemented debounced updates to modify the URL with current scroll positions without navigation.
|
||||
- Restored scroll positions from URL query parameters during initialization.
|
||||
|
||||
## 2025-06-16 - 1.0.97 - properties-panel
|
||||
- Improve element detection timing and value handling in properties panel
|
||||
|
||||
## 2025-06-16 - 1.0.96 - properties-panel
|
||||
- Enhance element detection and error handling for nested structures
|
||||
|
||||
## 2025-06-16 - 1.0.95 - package
|
||||
- Correct path for demotools export in package.json
|
||||
|
||||
## 2025-06-16 - 1.0.94 - demotools
|
||||
- Enhance runAfterRender to provide full DOM API access and improve element selection
|
||||
|
||||
## 2025-06-16 - 1.0.92 - demotools
|
||||
- Update DeesDemoWrapper to handle multiple slotted elements in runAfterRender callback
|
||||
|
||||
## 2025-06-16 - 1.0.91 - readme
|
||||
- Update documentation with comprehensive overview, quick start guide, and detailed feature descriptions
|
||||
|
||||
## 2025-06-16 - 1.0.90 - demo/properties/refactor
|
||||
- Add DeesDemoWrapper component for enhanced demo element handling
|
||||
- Enhance element detection in properties panel with recursive search and retry mechanism
|
||||
- Refactor code structure for improved readability and maintainability
|
||||
|
||||
## 2024-05-06 to 2020-05-10 - 1.0.89–1.0.17 - core
|
||||
- Over a series of releases, trivial core fixes and updates were applied.
|
||||
- (Note: Version 1.0.87 also included an update to the documentation.)
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--gitzone element-->
|
||||
<!-- made by Lossless GmbH -->
|
||||
<!-- made by Task Venture Capital GmbH -->
|
||||
<!-- checkout https://maintainedby.lossless.com for awesome OpenSource projects -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -9,7 +9,10 @@
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
|
||||
/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
|
||||
<!--Lets load standard fonts-->
|
||||
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -18,9 +21,8 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="../ts_web/index.ts"></script>
|
||||
<script type="module" src="/bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<wcc-dashboard></wcc-dashboard>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
10
html/index.ts
Normal file
10
html/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// dees tools
|
||||
import * as deesWccTools from '../ts_web/index.js';
|
||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||
|
||||
// elements and pages
|
||||
import * as elements from '../test/elements/index.js';
|
||||
import * as pages from '../test/pages/index.js';
|
||||
|
||||
deesWccTools.setupWccTools(elements as any, pages);
|
||||
deesDomTools.elementBasic.setup();
|
||||
@@ -1,18 +1,33 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"projectType": "element",
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "designestate",
|
||||
"gitrepo": "dees-wcctools",
|
||||
"shortDescription": "wcc tools for creating element catalogues",
|
||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||
"npmPackagename": "@designestate/dees-wcctools",
|
||||
"license": "MIT",
|
||||
"projectDomain": "design.estate"
|
||||
"projectDomain": "design.estate",
|
||||
"keywords": [
|
||||
"web components",
|
||||
"element catalogues",
|
||||
"custom elements",
|
||||
"documentation",
|
||||
"typescript",
|
||||
"lit",
|
||||
"component development",
|
||||
"design system",
|
||||
"element testing",
|
||||
"page development"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
||||
10452
package-lock.json
generated
10452
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@@ -1,33 +1,36 @@
|
||||
{
|
||||
"name": "@designestate/dees-wcctools",
|
||||
"version": "1.0.30",
|
||||
"name": "@design.estate/dees-wcctools",
|
||||
"version": "1.3.0",
|
||||
"private": false,
|
||||
"description": "wcc tools for creating element catalogues",
|
||||
"main": "dist_ts_web/index.js",
|
||||
"typings": "dist_ts_web/index.d.ts",
|
||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||
"exports": {
|
||||
".": "./dist_ts_web/index.js",
|
||||
"./demotools": "./dist_ts_demotools/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(npm run build)",
|
||||
"build": "(tsbuild custom ts_web --web && tsbundle element)",
|
||||
"watch": "tswatch element"
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
|
||||
"watch": "tswatch element",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@designestate/dees-domtools": "^1.0.26",
|
||||
"@gitzone/tsrun": "^1.1.17",
|
||||
"@pushrocks/smartdelay": "^2.0.9",
|
||||
"@pushrocks/smartexpress": "^3.0.10",
|
||||
"lit-element": "^2.0.0-rc.5",
|
||||
"lit-html": "^1.0.0-rc.2",
|
||||
"typescript": "^3.9.3"
|
||||
"@design.estate/dees-domtools": "^2.3.6",
|
||||
"@design.estate/dees-element": "^2.1.3",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"lit": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.24",
|
||||
"@gitzone/tsbundle": "^1.0.69",
|
||||
"@gitzone/tswatch": "^1.0.46",
|
||||
"@pushrocks/projectinfo": "^4.0.2",
|
||||
"tslint": "^6.1.2",
|
||||
"tslint-config-prettier": "^1.17.0"
|
||||
"@api.global/typedserver": "^7.11.1",
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@git.zone/tswatch": "^2.3.10",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -42,6 +45,19 @@
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions"
|
||||
]
|
||||
"last 1 Chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"web components",
|
||||
"element catalogues",
|
||||
"custom elements",
|
||||
"documentation",
|
||||
"typescript",
|
||||
"lit",
|
||||
"component development",
|
||||
"design system",
|
||||
"element testing",
|
||||
"page development"
|
||||
],
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||
}
|
||||
|
||||
9251
pnpm-lock.yaml
generated
Normal file
9251
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
176
readme.hints.md
Normal file
176
readme.hints.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Project Hints and Findings
|
||||
|
||||
## UI Redesign with Shadcn-like Styles (2025-06-27)
|
||||
|
||||
### Changes Made
|
||||
Updated the WCC Dashboard UI components (properties and sidebar) to use shadcn-like design patterns:
|
||||
|
||||
1. **Color System**: Implemented CSS variables for theming:
|
||||
- `--background`, `--foreground`, `--card`, `--primary`, `--secondary`
|
||||
- `--muted`, `--accent`, `--border`, `--input`, `--ring`
|
||||
- Consistent dark theme with subtle borders and proper contrast
|
||||
- Dynamic theme switching between light and dark modes
|
||||
|
||||
2. **Properties Panel Improvements (Updated)**:
|
||||
- Changed from fixed 3-column grid to flexible flexbox layout
|
||||
- Properties now wrap and use space more efficiently
|
||||
- Added rounded corners (using --radius-md) and better spacing
|
||||
- Property items use flexbox with min-width for responsive layout
|
||||
- Property labels now show as styled headers with type info
|
||||
- Form controls updated with shadcn-style focus states and transitions
|
||||
- Complex properties (Objects/Arrays) show "Edit" button
|
||||
- Advanced JSON editor appears above properties panel when editing complex types
|
||||
- Dynamic height adjustment (50px when editor is open, 120px normally)
|
||||
|
||||
3. **Sidebar Styling**:
|
||||
- Updated with consistent color scheme
|
||||
- Added rounded corners to menu items
|
||||
- Improved hover states with smooth transitions
|
||||
- Better typography with proper font weights
|
||||
|
||||
4. **Advanced Property Editor**:
|
||||
- JSON editor for complex types (Objects and Arrays)
|
||||
- Monaco-style monospace font for code editing
|
||||
- Live updates to element properties
|
||||
- Positioned above the properties panel with smooth transitions
|
||||
|
||||
5. **Theme and Viewport Selectors (New)**:
|
||||
- Redesigned buttons with flexbox layout for better icon/text alignment
|
||||
- Added hover effects with transform and shadow
|
||||
- Smooth transitions on all interactive elements
|
||||
- Selected state uses primary color variables
|
||||
- Icons reduced in size for better balance
|
||||
|
||||
6. **Form Controls (New)**:
|
||||
- Input fields and selects now have:
|
||||
- Rounded corners (--radius-sm)
|
||||
- Consistent padding (0.5rem 0.75rem)
|
||||
- Focus states with ring effect using box-shadow
|
||||
- Smooth transition animations
|
||||
- Checkboxes use accent-color for theming
|
||||
|
||||
### Technical Details
|
||||
- Uses system font stack ('Inter' preferred) for better native appearance
|
||||
- Subtle borders with CSS variables for consistency
|
||||
- Consistent spacing using rem units
|
||||
- Smooth transitions (0.2s ease) for interactive elements
|
||||
- Custom scrollbar styling for better visual integration
|
||||
- Grid layout with 1px gaps creating subtle dividers
|
||||
- Warning display with backdrop blur and rounded corners
|
||||
|
||||
## Advanced Complex Properties Editor (2025-06-27)
|
||||
|
||||
### Overview
|
||||
Implemented an advanced editor for complex properties (Arrays and Objects) that appears between the wcc-properties panel and frame when activated.
|
||||
|
||||
### Features
|
||||
1. **Dynamic Layout**: Frame shrinks by 300px from bottom when editor opens
|
||||
2. **Multiple Editors**: Can edit multiple properties simultaneously side by side
|
||||
3. **JSON Editor**:
|
||||
- Monospace font for code editing
|
||||
- Tab key support for indentation
|
||||
- Syntax validation with error messages
|
||||
- Live preview of changes
|
||||
4. **Smooth Transitions**: Animated opening/closing with 0.3s ease
|
||||
5. **Error Handling**: Invalid JSON shows clear error messages that disappear on typing
|
||||
6. **Close All Button**: Single button to close all open editors at once
|
||||
|
||||
### Technical Implementation (Updated)
|
||||
- **State Management**: Changed from single editor to array of editors with unique IDs
|
||||
- **Editor Structure**: Each editor instance contains:
|
||||
- `id`: Unique identifier (`propertyName-timestamp`)
|
||||
- `name`: Property name
|
||||
- `value`: Original value
|
||||
- `element`: Reference to the element
|
||||
- `editorValue`: Current JSON string
|
||||
- `editorError`: Validation error message
|
||||
- **Event System**: Uses custom 'editorStateChanged' event to communicate with parent dashboard
|
||||
- **Dynamic Styling**: wcc-frame's bottom position changes from 100px to 400px when any editor is open
|
||||
- **Property Types**: Object and Array properties show "Edit Object/Array" button instead of inline controls
|
||||
|
||||
### User Flow
|
||||
1. Click "Edit Object/Array" button on complex property
|
||||
2. Editor slides up between properties panel and frame
|
||||
3. Click additional "Edit" buttons to open more properties side by side
|
||||
4. Each editor can be saved/cancelled independently
|
||||
5. "Close All" button dismisses all editors at once
|
||||
6. Frame automatically resizes back when all editors are closed
|
||||
|
||||
### Layout Details
|
||||
- **Container**: Flexbox with horizontal scrolling when multiple editors overflow
|
||||
- **Editor Width**: Min 300px, max 500px, flexible between
|
||||
- **Scrollbar**: Custom styled thin scrollbar for horizontal overflow
|
||||
- **Header Bar**: Fixed top bar with "Property Editors" title and "Close All" button
|
||||
|
||||
## Properties Panel Element Detection Issue (Fixed)
|
||||
|
||||
### Problem
|
||||
The properties panel had timing issues detecting rendered elements because:
|
||||
1. Elements are rendered asynchronously via lit's `render()` function in the dashboard component
|
||||
2. The properties panel tried to find elements immediately without waiting for render completion
|
||||
3. Element search only looked at direct children of the viewport, missing nested elements or those inside shadow DOM
|
||||
|
||||
### Solution Implemented
|
||||
1. Added a 100ms initial delay to allow render completion
|
||||
2. Implemented recursive element search that:
|
||||
- Searches through nested children up to 5 levels deep
|
||||
- Checks both light DOM and shadow DOM for all elements
|
||||
- Handles complex DOM structures generically
|
||||
- Works with any wrapper elements, not specific to dees-demowrapper
|
||||
3. Added retry mechanism with up to 5 attempts (200ms between retries)
|
||||
4. Improved error messages to show retry count
|
||||
5. Comprehensive error handling:
|
||||
- Errors in element search don't break the update cycle
|
||||
- Individual property errors don't prevent other properties from rendering
|
||||
- scheduleUpdate always completes even if createProperties fails
|
||||
- Clears warnings and property content appropriately on errors
|
||||
|
||||
### Code Flow
|
||||
1. Dashboard renders element demo into viewport using `render(anonItem.demo(), viewport)`
|
||||
2. Properties panel waits 200ms for demo wrappers to run and set initial values
|
||||
3. Searches recursively for the element instance
|
||||
4. If not found, retries with delays to handle async rendering
|
||||
5. Once found, extracts and displays element properties
|
||||
6. Uses property binding (`.value=`) instead of attribute binding to prevent input events during initialization
|
||||
|
||||
## Demo Tools
|
||||
|
||||
### DeesDemoWrapper Component
|
||||
A utility component for wrapping demo elements with post-render functionality.
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import * as demoTools from '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
// In your demo function:
|
||||
demo: () => html`
|
||||
<dees-demowrapper .runAfterRender=${(wrapper) => {
|
||||
// Use querySelector for specific elements
|
||||
const myElement = wrapper.querySelector('my-custom-element');
|
||||
myElement?.setAttribute('data-demo', 'true');
|
||||
|
||||
// Access all children
|
||||
console.log('All children:', wrapper.children);
|
||||
|
||||
// Use querySelectorAll for multiple elements
|
||||
wrapper.querySelectorAll('div').forEach(div => {
|
||||
console.log('Found div:', div);
|
||||
});
|
||||
|
||||
// Full DOM API available
|
||||
const firstChild = wrapper.firstElementChild;
|
||||
const hasClass = wrapper.querySelector('.my-class');
|
||||
}}>
|
||||
<my-custom-element></my-custom-element>
|
||||
<div>Additional content</div>
|
||||
</dees-demowrapper>
|
||||
`
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Wraps demo elements without affecting layout (uses `display: contents`)
|
||||
- Provides the wrapper element itself with full DOM API access
|
||||
- Use querySelector/querySelectorAll for powerful element selection
|
||||
- Access children via wrapper.children property
|
||||
- Supports async operations in runAfterRender callback
|
||||
- Automatically handles timing to ensure elements are fully rendered
|
||||
410
readme.md
410
readme.md
@@ -1,35 +1,397 @@
|
||||
# @designestate/dees-wcctools
|
||||
wcc tools for creating element catalogues
|
||||
# @design.estate/dees-wcctools
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@designestate/dees-wcctools)
|
||||
* [gitlab.com (source)](https://gitlab.com/designestate/dees-wcctools)
|
||||
* [github.com (source mirror)](https://github.com/designestate/dees-wcctools)
|
||||
* [docs (typedoc)](https://designestate.gitlab.io/dees-wcctools/)
|
||||
🛠️ **Web Component Development Tools** — A powerful framework for building, testing, documenting, and recording web components
|
||||
|
||||
## Status for master
|
||||
[](https://gitlab.com/designestate/dees-wcctools/commits/master)
|
||||
[](https://gitlab.com/designestate/dees-wcctools/commits/master)
|
||||
[](https://www.npmjs.com/package/@designestate/dees-wcctools)
|
||||
[](https://snyk.io/test/npm/@designestate/dees-wcctools)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://prettier.io/)
|
||||
## Overview
|
||||
|
||||
## Usage
|
||||
`@design.estate/dees-wcctools` provides a comprehensive development environment for web components, featuring:
|
||||
|
||||
## Contribution
|
||||
- 🎨 **Interactive Component Catalogue** — Live preview with sidebar navigation
|
||||
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
||||
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
||||
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
||||
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
||||
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
||||
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Contribution
|
||||
## Installation
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
```bash
|
||||
# Using pnpm (recommended)
|
||||
pnpm add -D @design.estate/dees-wcctools
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
# Using npm
|
||||
npm install @design.estate/dees-wcctools --save-dev
|
||||
```
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
## Quick Start
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
### 1. Create Your Component
|
||||
|
||||
```typescript
|
||||
import { DeesElement, customElement, html, css, property } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('my-button')
|
||||
export class MyButton extends DeesElement {
|
||||
// Define a demo for the catalogue
|
||||
public static demo = () => html`
|
||||
<my-button .label=${'Click me!'} .variant=${'primary'}></my-button>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string = 'Button';
|
||||
|
||||
@property({ type: String })
|
||||
accessor variant: 'primary' | 'secondary' = 'primary';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
button.primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
button.secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<button class="${this.variant}">
|
||||
${this.label}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Set Up Your Catalogue
|
||||
|
||||
```typescript
|
||||
// catalogue.ts
|
||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import { html } from 'lit';
|
||||
|
||||
// Import your components
|
||||
import { MyButton } from './components/my-button.js';
|
||||
import { MyCard } from './components/my-card.js';
|
||||
|
||||
// Define elements for the catalogue
|
||||
const elements = {
|
||||
'my-button': MyButton,
|
||||
'my-card': MyCard,
|
||||
};
|
||||
|
||||
// Optionally define pages
|
||||
const pages = {
|
||||
'home': () => html`
|
||||
<div style="padding: 20px;">
|
||||
<h1>Welcome to My Component Library</h1>
|
||||
<p>Browse components using the sidebar.</p>
|
||||
</div>
|
||||
`,
|
||||
'getting-started': () => html`
|
||||
<div style="padding: 20px;">
|
||||
<h2>Getting Started</h2>
|
||||
<p>Installation and usage instructions...</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
// Initialize the catalogue
|
||||
setupWccTools(elements, pages);
|
||||
```
|
||||
|
||||
### 3. Create an HTML Entry Point
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Component Catalogue</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<script type="module" src="./catalogue.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 Live Property Editing
|
||||
|
||||
The properties panel automatically detects and allows editing of:
|
||||
|
||||
| Property Type | Editor |
|
||||
|--------------|--------|
|
||||
| **String** | Text input |
|
||||
| **Number** | Number input |
|
||||
| **Boolean** | Checkbox |
|
||||
| **Enum** | Select dropdown |
|
||||
| **Object/Array** | JSON editor modal |
|
||||
|
||||
### 📱 Viewport Testing
|
||||
|
||||
Test your components across different screen sizes:
|
||||
|
||||
- **Phone** — 320px width
|
||||
- **Phablet** — 600px width
|
||||
- **Tablet** — 768px width
|
||||
- **Desktop** — Full width (native)
|
||||
|
||||
### 🌓 Theme Support
|
||||
|
||||
Components automatically adapt to light/dark themes using the `goBright` property:
|
||||
|
||||
```typescript
|
||||
public render() {
|
||||
return html`
|
||||
<div class="${this.goBright ? 'light-theme' : 'dark-theme'}">
|
||||
<!-- Your component content -->
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
Or use CSS custom properties with the theme manager:
|
||||
|
||||
```typescript
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
background: ${cssManager.bdTheme('#fff', '#000')};
|
||||
}
|
||||
`
|
||||
];
|
||||
```
|
||||
|
||||
### 🎬 Screen Recording
|
||||
|
||||
Record component demos directly from the catalogue! The built-in recorder supports:
|
||||
|
||||
- **Viewport Recording** — Record just the component viewport
|
||||
- **Full Screen Recording** — Capture the entire screen
|
||||
- **Audio Support** — Add microphone commentary with live level monitoring
|
||||
- **Video Trimming** — Trim start/end before export with visual timeline
|
||||
- **WebM Export** — High-quality video output
|
||||
|
||||
Click the red record button in the bottom toolbar to start.
|
||||
|
||||
### 🧪 Demo Tools
|
||||
|
||||
The demotools module provides enhanced testing capabilities with `dees-demowrapper`:
|
||||
|
||||
```typescript
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
@customElement('my-component')
|
||||
export class MyComponent extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (wrapper) => {
|
||||
// Find elements using standard DOM APIs
|
||||
const myComponent = wrapper.querySelector('my-component');
|
||||
|
||||
// Simulate user interactions
|
||||
myComponent.value = 'Test value';
|
||||
await myComponent.updateComplete;
|
||||
|
||||
// Work with multiple elements
|
||||
wrapper.querySelectorAll('.item').forEach((el, i) => {
|
||||
console.log(`Item ${i}:`, el.textContent);
|
||||
});
|
||||
}}>
|
||||
<my-component></my-component>
|
||||
<div class="item">Item 1</div>
|
||||
<div class="item">Item 2</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### ⏳ Async Demos
|
||||
|
||||
Return a `Promise` from `demo` for async setup. The dashboard waits for resolution:
|
||||
|
||||
```typescript
|
||||
public static demo = async () => {
|
||||
const data = await fetchSomeData();
|
||||
return html`<my-component .data=${data}></my-component>`;
|
||||
};
|
||||
```
|
||||
|
||||
### 🎭 Container Queries
|
||||
|
||||
Components can respond to their container size using the `wccToolsViewport` container:
|
||||
|
||||
```typescript
|
||||
public static styles = [
|
||||
css`
|
||||
@container wccToolsViewport (min-width: 768px) {
|
||||
:host {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@container wccToolsViewport (max-width: 767px) {
|
||||
:host {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
`
|
||||
];
|
||||
```
|
||||
|
||||
## Component Guidelines
|
||||
|
||||
### Required for Catalogue Display
|
||||
|
||||
1. Components must expose a static `demo` property returning a Lit template
|
||||
2. Use `@property()` decorators with the `accessor` keyword for editable properties
|
||||
3. Export component classes for proper detection
|
||||
|
||||
### Best Practices
|
||||
|
||||
```typescript
|
||||
@customElement('best-practice-component')
|
||||
export class BestPracticeComponent extends DeesElement {
|
||||
// ✅ Static demo property
|
||||
public static demo = () => html`
|
||||
<best-practice-component
|
||||
.complexProp=${{ key: 'value' }}
|
||||
simpleAttribute="test"
|
||||
></best-practice-component>
|
||||
`;
|
||||
|
||||
// ✅ Typed properties with defaults (TC39 decorators)
|
||||
@property({ type: String })
|
||||
accessor title: string = 'Default Title';
|
||||
|
||||
// ✅ Complex property without attribute
|
||||
@property({ attribute: false })
|
||||
accessor complexProp: { key: string } = { key: 'default' };
|
||||
|
||||
// ✅ Enum with proper typing
|
||||
@property({ type: String })
|
||||
accessor variant: 'small' | 'medium' | 'large' = 'medium';
|
||||
}
|
||||
```
|
||||
|
||||
## URL Routing
|
||||
|
||||
The catalogue uses URL routing for deep linking:
|
||||
|
||||
```
|
||||
/wcctools-route/:type/:name/:viewport/:theme
|
||||
|
||||
Examples:
|
||||
/wcctools-route/element/my-button/desktop/dark
|
||||
/wcctools-route/page/home/tablet/bright
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `setupWccTools(elements, pages?)`
|
||||
|
||||
Initialize the WCC Tools dashboard.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `elements` | `Record<string, typeof LitElement>` | Map of element names to classes |
|
||||
| `pages` | `Record<string, TTemplateFactory>` | Optional map of page names to template functions |
|
||||
|
||||
### `DeesDemoWrapper`
|
||||
|
||||
Component for wrapping demos with post-render logic.
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `runAfterRender` | `(wrapper) => void \| Promise<void>` | Callback after wrapped elements render |
|
||||
|
||||
The wrapper provides full DOM API access:
|
||||
- `wrapper.querySelector()` — Find single element
|
||||
- `wrapper.querySelectorAll()` — Find multiple elements
|
||||
- `wrapper.children` — Access child elements directly
|
||||
|
||||
### Recording Components (Advanced)
|
||||
|
||||
For custom recording integrations:
|
||||
|
||||
```typescript
|
||||
import { RecorderService } from '@design.estate/dees-wcctools';
|
||||
|
||||
const recorder = new RecorderService({
|
||||
onDurationUpdate: (duration) => console.log(`${duration}s`),
|
||||
onRecordingComplete: (blob) => console.log('Recording done!', blob),
|
||||
onAudioLevelUpdate: (level) => console.log(`Audio: ${level}%`),
|
||||
});
|
||||
|
||||
await recorder.startRecording({ mode: 'viewport' });
|
||||
// ... later
|
||||
recorder.stopRecording();
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my-components/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── my-button.ts
|
||||
│ │ └── my-card.ts
|
||||
│ └── catalogue.ts
|
||||
├── dist/
|
||||
├── index.html
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- ✅ Chrome/Edge (latest)
|
||||
- ✅ Firefox (latest)
|
||||
- ✅ Safari (latest)
|
||||
- ✅ Mobile browsers with Web Components support
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
107
readme.plan.md
Normal file
107
readme.plan.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Fix Properties Panel Element Detection (COMPLETED)
|
||||
|
||||
To fix the element detection issue, reread CLAUDE.md first.
|
||||
|
||||
## Problem Analysis
|
||||
The properties panel has timing issues detecting rendered elements because:
|
||||
1. Elements are rendered asynchronously via lit's `render()` in the dashboard
|
||||
2. Properties panel tries to find elements immediately without waiting for render completion
|
||||
3. Element search only looks at direct children, missing nested/shadow DOM elements
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Add proper synchronization ✅
|
||||
- Add a delay or await render completion before element detection
|
||||
- Use MutationObserver or lit's updateComplete promises
|
||||
|
||||
### 2. Improve element search algorithm ✅
|
||||
- Search recursively through all descendants, not just direct children
|
||||
- Handle shadow DOM boundaries properly
|
||||
- Support elements wrapped in containers
|
||||
|
||||
### 3. Add retry mechanism ✅
|
||||
- If element not found, retry after a delay
|
||||
- Add maximum retry attempts to prevent infinite loops
|
||||
- Clear error state when element is eventually found
|
||||
|
||||
## Code Changes Required
|
||||
1. Modify `wcc-properties.ts` createProperties() method ✅
|
||||
2. Add element search utility function ✅
|
||||
3. Improve error handling and user feedback ✅
|
||||
|
||||
# Demo Wrapper Implementation (COMPLETED)
|
||||
|
||||
## Created DeesDemoWrapper Component
|
||||
- Location: ts_demotools/demotools.ts
|
||||
- Allows wrapping demo elements with post-render functionality
|
||||
- Provides runAfterRender callback that receives ALL slotted elements as HTMLCollection
|
||||
- Uses display: contents to not affect layout
|
||||
- Handles timing automatically with 50ms delay after firstUpdated
|
||||
- Supports both sync and async callbacks
|
||||
- Exports available at @design.estate/dees-wcctools/demotools (lowercase)
|
||||
|
||||
# Documentation Update (COMPLETED)
|
||||
|
||||
## Updated readme.md with:
|
||||
- Comprehensive overview with feature highlights
|
||||
- Quick start guide with code examples
|
||||
- Detailed feature documentation
|
||||
- Advanced demo tools usage
|
||||
- Best practices and guidelines
|
||||
- API reference
|
||||
- Browser support information
|
||||
- Complete examples for all major features
|
||||
|
||||
# Enhanced DemoWrapper (COMPLETED)
|
||||
|
||||
## Modified runAfterRender callback:
|
||||
- Now receives the wrapper element itself instead of just children
|
||||
- Provides full DOM API access (querySelector, querySelectorAll, etc.)
|
||||
- querySelector works on slotted content (light DOM children)
|
||||
- Access children via wrapper.children property
|
||||
- Updated documentation with correct import path (lowercase 'demotools')
|
||||
- Examples show how to use querySelector for powerful element selection
|
||||
- Added clarifying comment about querySelector working on slotted content
|
||||
|
||||
## Fixed Properties Panel Compatibility:
|
||||
- Made element search generic - works with any container elements
|
||||
- Searches both light DOM and shadow DOM recursively
|
||||
- Improved error handling to prevent breaking the update cycle
|
||||
- Errors in one property don't prevent others from rendering
|
||||
- Detection continues working even after errors occur
|
||||
- Maintains compatibility with all element structures
|
||||
|
||||
# Test Elements Created (COMPLETED)
|
||||
|
||||
## Created comprehensive test elements:
|
||||
1. **test-noprops** - Element with no @property decorators
|
||||
2. **test-complextypes** - Element with arrays, objects, dates, and complex nested data
|
||||
3. **test-withwrapper** - Element that uses dees-demowrapper in its demo
|
||||
4. **test-edgecases** - Element with edge cases (null, undefined, NaN, Infinity, circular refs)
|
||||
5. **test-nested** - Element with deeply nested structure to test recursive search
|
||||
|
||||
These test various scenarios:
|
||||
- Properties panel handling of elements without properties
|
||||
- Complex data type display and editing
|
||||
- Element detection inside dees-demowrapper
|
||||
- Error handling for problematic values
|
||||
- Deep nesting and shadow DOM traversal
|
||||
|
||||
# Fixed Demo Value Overwriting (COMPLETED)
|
||||
|
||||
## Issue:
|
||||
Properties panel was overwriting values set by demo functions
|
||||
|
||||
## Solution:
|
||||
1. Changed from attribute binding (`value=`) to property binding (`.value=`)
|
||||
2. This prevents browser from firing input events during initialization
|
||||
3. Added proper number parsing for number inputs
|
||||
4. Increased initial wait to 200ms for demo wrappers to complete
|
||||
5. Simplified select element handling to use property binding
|
||||
# Async Demo Support (IN PROGRESS)
|
||||
|
||||
## Tasks
|
||||
- [ ] Allow dashboard-selected items to return Promise-based TemplateResults
|
||||
- [ ] Await async demos/pages before rendering them into the viewport
|
||||
- [ ] Add regression test covering async demo usage
|
||||
- [ ] Document async demo pattern in README and verify with pnpm scripts
|
||||
6
test/elements/index.ts
Normal file
6
test/elements/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './test-demoelement.js';
|
||||
export * from './test-noprops.js';
|
||||
export * from './test-complextypes.js';
|
||||
export * from './test-withwrapper.js';
|
||||
export * from './test-edgecases.js';
|
||||
export * from './test-nested.js';
|
||||
137
test/elements/test-complextypes.ts
Normal file
137
test/elements/test-complextypes.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
interface IComplexData {
|
||||
name: string;
|
||||
age: number;
|
||||
tags: string[];
|
||||
metadata: {
|
||||
created: Date;
|
||||
modified: Date;
|
||||
author: string;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement('test-complextypes')
|
||||
export class TestComplexTypes extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<test-complextypes
|
||||
.complexData=${{
|
||||
name: 'Test User',
|
||||
age: 25,
|
||||
tags: ['developer', 'designer'],
|
||||
metadata: {
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
author: 'System'
|
||||
}
|
||||
}}
|
||||
></test-complextypes>
|
||||
`;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor stringArray: string[] = ['apple', 'banana', 'cherry'];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor numberArray: number[] = [1, 2, 3, 4, 5];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor complexData: IComplexData = {
|
||||
name: 'Default Name',
|
||||
age: 0,
|
||||
tags: [],
|
||||
metadata: {
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
author: 'Unknown'
|
||||
}
|
||||
};
|
||||
|
||||
@property({ type: Object })
|
||||
accessor simpleObject = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
key3: 123
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor functionProperty = () => {
|
||||
console.log('This is a function property');
|
||||
};
|
||||
|
||||
@property({ type: Date })
|
||||
accessor dateProperty = new Date();
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.section {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.value {
|
||||
color: #666;
|
||||
margin-left: 10px;
|
||||
}
|
||||
pre {
|
||||
background: #f0f0f0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="section">
|
||||
<span class="label">String Array:</span>
|
||||
<span class="value">${this.stringArray.join(', ')}</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Number Array:</span>
|
||||
<span class="value">${this.numberArray.join(', ')}</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Complex Data:</span>
|
||||
<pre>${JSON.stringify(this.complexData, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Simple Object:</span>
|
||||
<pre>${JSON.stringify(this.simpleObject, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Date Property:</span>
|
||||
<span class="value">${this.dateProperty.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Function Property:</span>
|
||||
<span class="value">${typeof this.functionProperty}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
112
test/elements/test-demoelement.ts
Normal file
112
test/elements/test-demoelement.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
enum ETestEnum {
|
||||
'first' = 'first',
|
||||
'second' = 'second',
|
||||
'awesome' = 'awesome',
|
||||
}
|
||||
|
||||
@customElement('test-demoelement')
|
||||
export class TestDemoelement extends DeesElement {
|
||||
public static demo = () => html`<test-demoelement>This is a slot text</test-demoelement>`;
|
||||
|
||||
@property()
|
||||
accessor notTyped = 'hello';
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor typedAndNotInitizalized: string;
|
||||
|
||||
@property()
|
||||
accessor notTypedAndNotInitizalized: string;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
accessor demoBoolean = false;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor demoString = 'default demo string';
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
accessor demoNumber = 2;
|
||||
|
||||
@property({
|
||||
type: ETestEnum,
|
||||
})
|
||||
accessor demoENum: ETestEnum = ETestEnum.first;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
.maincontainer,
|
||||
.themeindicator {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.maincontainer {
|
||||
color: #fff;
|
||||
background: #000;
|
||||
}
|
||||
.themeindicator {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
background: ${cssManager.bdTheme('#fff', '#000')};
|
||||
}
|
||||
|
||||
@container wccToolsViewport size(min-width: 1px) {
|
||||
.maincontainer,
|
||||
.themeindicator {
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
${domtools.breakpoints.cssForPhablet(css`
|
||||
.maincontainer,
|
||||
.themeindicator {
|
||||
border-radius: 50px;
|
||||
}
|
||||
`)}
|
||||
|
||||
pre b {
|
||||
color: green;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<style></style>
|
||||
<div class="maincontainer"><slot>This is a demo element</slot></div>
|
||||
<div class="themeindicator">
|
||||
You have selected the ${this.goBright ? 'bright' : 'dark'} theme.
|
||||
<pre>
|
||||
demoBoolean is <b>${this.demoBoolean}</b>
|
||||
demoString is <b>"${this.demoString}"</b>
|
||||
demoNumber is <b>${this.demoNumber}</b>
|
||||
demoEnum is <b>"${this.demoENum}"</b>
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
195
test/elements/test-edgecases.ts
Normal file
195
test/elements/test-edgecases.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-edgecases')
|
||||
export class TestEdgeCases extends DeesElement {
|
||||
public static demo = () => html`<test-edgecases></test-edgecases>`;
|
||||
|
||||
// Property with null value
|
||||
@property({ type: String })
|
||||
accessor nullableString: string | null = null;
|
||||
|
||||
// Property with undefined value
|
||||
@property({ type: Number })
|
||||
accessor undefinedNumber: number | undefined = undefined;
|
||||
|
||||
// Very long string
|
||||
@property({ type: String })
|
||||
accessor longString: string = 'Lorem ipsum '.repeat(50);
|
||||
|
||||
// Property with special characters
|
||||
@property({ type: String })
|
||||
accessor specialChars: string = '!@#$%^&*()_+-=[]{}|;\':",./<>?`~';
|
||||
|
||||
// Property that could cause rendering issues
|
||||
@property({ type: String })
|
||||
accessor htmlString: string = '<script>alert("test")</script><b>Bold text</b>';
|
||||
|
||||
// Numeric edge cases
|
||||
@property({ type: Number })
|
||||
accessor infinityNumber: number = Infinity;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor nanNumber: number = NaN;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor veryLargeNumber: number = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor verySmallNumber: number = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor floatNumber: number = 3.14159265359;
|
||||
|
||||
// Boolean-like values
|
||||
@property({ type: String })
|
||||
accessor booleanString: string = 'false';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor booleanNumber: number = 0;
|
||||
|
||||
// Empty values
|
||||
@property({ type: String })
|
||||
accessor emptyString: string = '';
|
||||
|
||||
@property({ type: Array })
|
||||
accessor emptyArray: any[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
accessor emptyObject: {} = {};
|
||||
|
||||
// Circular reference (should not break properties panel)
|
||||
@property({ attribute: false })
|
||||
accessor circularRef: any = (() => {
|
||||
const obj: any = { name: 'circular' };
|
||||
obj.self = obj;
|
||||
return obj;
|
||||
})();
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #fff3e0;
|
||||
border: 2px solid #ff9800;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.warning {
|
||||
background: #ffe0b2;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
color: #e65100;
|
||||
}
|
||||
.property {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #f57c00;
|
||||
}
|
||||
.value {
|
||||
color: #666;
|
||||
}
|
||||
.special {
|
||||
background: #ffccbc;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
private formatValue(value: any): string {
|
||||
if (value === null) return 'null';
|
||||
if (value === undefined) return 'undefined';
|
||||
if (value === Infinity) return 'Infinity';
|
||||
if (Number.isNaN(value)) return 'NaN';
|
||||
if (typeof value === 'string' && value.length > 50) {
|
||||
return value.substring(0, 50) + '...';
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (e) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="warning">
|
||||
⚠️ This element tests edge cases and problematic values
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Nullable String:</span>
|
||||
<span class="value special">${this.formatValue(this.nullableString)}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Undefined Number:</span>
|
||||
<span class="value special">${this.formatValue(this.undefinedNumber)}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Long String:</span>
|
||||
<span class="value">${this.formatValue(this.longString)}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Special Characters:</span>
|
||||
<span class="value">${this.specialChars}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">HTML String (escaped):</span>
|
||||
<span class="value">${this.htmlString}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Infinity:</span>
|
||||
<span class="value special">${this.formatValue(this.infinityNumber)}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">NaN:</span>
|
||||
<span class="value special">${this.formatValue(this.nanNumber)}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Very Large Number:</span>
|
||||
<span class="value">${this.veryLargeNumber}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Float Number:</span>
|
||||
<span class="value">${this.floatNumber}</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Empty String:</span>
|
||||
<span class="value special">[empty]</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">Circular Reference:</span>
|
||||
<span class="value special">${this.formatValue(this.circularRef)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
127
test/elements/test-nested.ts
Normal file
127
test/elements/test-nested.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
// Helper component for nesting
|
||||
@customElement('test-nested-wrapper')
|
||||
class TestNestedWrapper extends DeesElement {
|
||||
public render() {
|
||||
return html`
|
||||
<div style="border: 1px dashed #ccc; padding: 10px; margin: 5px;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// The actual test element deeply nested
|
||||
@customElement('test-nested-target')
|
||||
class TestNestedTarget extends DeesElement {
|
||||
@property({ type: String })
|
||||
accessor message: string = 'I am deeply nested!';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor depth: number = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor found: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background: #e1f5fe;
|
||||
border: 2px solid #0288d1;
|
||||
border-radius: 4px;
|
||||
margin: 5px;
|
||||
}
|
||||
.info {
|
||||
font-family: monospace;
|
||||
color: #01579b;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="info">
|
||||
<strong>Nested Target Element</strong><br>
|
||||
Message: ${this.message}<br>
|
||||
Depth: ${this.depth}<br>
|
||||
Found by properties panel: ${this.found ? '✅' : '❌'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('test-nested')
|
||||
export class TestNested extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<test-nested></test-nested>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor testId: string = 'nested-test';
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
border: 2px solid #999;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.explanation {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.structure {
|
||||
background: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="explanation">
|
||||
<h3>Nested Structure Test</h3>
|
||||
<p>The actual element with properties is nested deep inside multiple layers:</p>
|
||||
</div>
|
||||
|
||||
<div class="structure">
|
||||
<test-nested-wrapper>
|
||||
<div style="padding: 10px; background: #ffe;">
|
||||
<test-nested-wrapper>
|
||||
<div style="padding: 10px; background: #efe;">
|
||||
<test-nested-wrapper>
|
||||
<div style="padding: 10px; background: #eef;">
|
||||
<!-- The target element is here, 3 levels deep -->
|
||||
<test-nested-target
|
||||
.message=${'Found me at depth 3!'}
|
||||
.depth=${3}
|
||||
></test-nested-target>
|
||||
</div>
|
||||
</test-nested-wrapper>
|
||||
</div>
|
||||
</test-nested-wrapper>
|
||||
</div>
|
||||
</test-nested-wrapper>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; font-style: italic; color: #666;">
|
||||
Properties panel should find the test-nested-target element despite the deep nesting.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
37
test/elements/test-noprops.ts
Normal file
37
test/elements/test-noprops.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('test-noprops')
|
||||
export class TestNoProps extends DeesElement {
|
||||
public static demo = () => html`<test-noprops></test-noprops>`;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.message {
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="message">
|
||||
This element has no @property decorators.
|
||||
Properties panel should handle this gracefully.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
111
test/elements/test-withwrapper.ts
Normal file
111
test/elements/test-withwrapper.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
// Import from local demotools
|
||||
import '../../ts_demotools/demotools.js';
|
||||
|
||||
@customElement('test-withwrapper')
|
||||
export class TestWithWrapper extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (wrapper) => {
|
||||
console.log('DemoWrapper: Found wrapper element', wrapper);
|
||||
|
||||
const testElement = wrapper.querySelector('test-withwrapper');
|
||||
if (testElement) {
|
||||
console.log('DemoWrapper: Found test-withwrapper element');
|
||||
testElement.dynamicValue = 'Set by demo wrapper!';
|
||||
testElement.counter = 100;
|
||||
|
||||
// Test querySelector functionality
|
||||
const innerDiv = wrapper.querySelector('.inner-content');
|
||||
console.log('DemoWrapper: Found inner div:', innerDiv);
|
||||
|
||||
// Test querySelectorAll
|
||||
const allButtons = wrapper.querySelectorAll('button');
|
||||
console.log(`DemoWrapper: Found ${allButtons.length} buttons`);
|
||||
}
|
||||
}}>
|
||||
<test-withwrapper></test-withwrapper>
|
||||
<div style="margin-top: 10px; padding: 10px; background: #e0e0e0;">
|
||||
This div is also inside the wrapper
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
|
||||
@property({ type: String })
|
||||
accessor dynamicValue: string = 'Initial value';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor counter: number = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor isActive: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: #e8f5e9;
|
||||
border: 2px solid #4caf50;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.wrapper-info {
|
||||
background: #c8e6c9;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.inner-content {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
button {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="wrapper-info">
|
||||
This element is wrapped with dees-demowrapper in its demo
|
||||
</div>
|
||||
|
||||
<div class="inner-content">
|
||||
<h3>Dynamic Value: ${this.dynamicValue}</h3>
|
||||
<p>Counter: ${this.counter}</p>
|
||||
<p>Active: ${this.isActive ? 'Yes' : 'No'}</p>
|
||||
|
||||
<button @click=${() => this.counter++}>Increment</button>
|
||||
<button @click=${() => this.isActive = !this.isActive}>Toggle Active</button>
|
||||
<button @click=${() => this.dynamicValue = 'Clicked!'}>Change Value</button>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
Properties panel should detect this element inside the wrapper
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
3
test/index.ts
Normal file
3
test/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as wcctools from '../ts_web/index.js';
|
||||
import * as elements from './elements/index.js';
|
||||
wcctools.setupWccTools(elements as any, {});
|
||||
2
test/pages/index.ts
Normal file
2
test/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './page1.js';
|
||||
export * from './pageLongScroll.js';
|
||||
3
test/pages/page1.ts
Normal file
3
test/pages/page1.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const page1 = () => html` <test-demoelement></test-demoelement> `;
|
||||
138
test/pages/pageLongScroll.ts
Normal file
138
test/pages/pageLongScroll.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const pageLongScroll = () => html`
|
||||
<style>
|
||||
.long-scroll-container {
|
||||
padding: 40px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 60px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
margin-bottom: 15px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
height: 300px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888;
|
||||
font-size: 1.2em;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="long-scroll-container">
|
||||
<h1>Long Scroll Test Page</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 1: Introduction</h2>
|
||||
<p>This is a long page designed to test scroll position preservation. Scroll down to see more content and then reload the page to verify that the scroll position is restored correctly.</p>
|
||||
<p>The URL should update with scroll position parameters as you scroll, and when you reload the page, it should automatically scroll to the last position.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 1</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 2: Testing Scroll Behavior</h2>
|
||||
<p>As you scroll through this page, the dashboard should track your scroll position and update the URL accordingly. The updates should be debounced to avoid excessive URL changes.</p>
|
||||
<p>Try scrolling quickly and slowly to see how the debouncing works. The URL should update smoothly without interfering with your scrolling experience.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 2</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 3: Reload Testing</h2>
|
||||
<p>Once you've scrolled to this section, try reloading the page. The page should automatically scroll back to this position after the content loads.</p>
|
||||
<p>This demonstrates that the scroll position is being preserved across page reloads using URL parameters.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 3</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 4: Navigation Testing</h2>
|
||||
<p>Try navigating to a different element or page in the sidebar, then use the browser's back button to return here. The scroll position should be preserved.</p>
|
||||
<p>This tests that the browser history correctly maintains scroll state for each navigation entry.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 4</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 5: Deep Scroll Testing</h2>
|
||||
<p>Keep scrolling! This page has plenty of content to ensure we can test scroll positions at various depths.</p>
|
||||
<p>The scroll tracking should work reliably regardless of how far down the page you scroll.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 5</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 6: Performance Testing</h2>
|
||||
<p>Even with continuous scroll tracking, the page should remain responsive and smooth. The debouncing mechanism ensures that URL updates don't impact scrolling performance.</p>
|
||||
<p>Try scrolling rapidly up and down to verify that the scrolling remains smooth.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 6</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 7: Sidebar Scroll Testing</h2>
|
||||
<p>Don't forget to test the sidebar scrolling as well! If the sidebar has enough items to scroll, its position should also be tracked and restored.</p>
|
||||
<p>Both the main content and sidebar scroll positions should be preserved independently.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 7</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 8: Edge Cases</h2>
|
||||
<p>This section tests edge cases like scrolling to the very bottom of the page, then reloading.</p>
|
||||
<p>The scroll restoration should handle these cases gracefully without any visual glitches or errors.</p>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-content">
|
||||
<span>Placeholder Content Block 8</span>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Section 9: Final Section</h2>
|
||||
<p>You've reached the end of the scroll test page! Try reloading from here to ensure that even the bottom-most scroll positions are correctly preserved.</p>
|
||||
<p>The scroll position tracking has been successfully implemented if you can reload and return to this exact position.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
22
test/test.demoresolver.node.ts
Normal file
22
test/test.demoresolver.node.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { resolveTemplateFactory } from '../ts_web/elements/wcctools.helpers.js';
|
||||
import { html } from 'lit';
|
||||
|
||||
const waitFor = (durationMs: number) => new Promise(resolve => setTimeout(resolve, durationMs));
|
||||
|
||||
tap.test('resolveTemplateFactory returns sync TemplateResult', async () => {
|
||||
const template = html`<p>sync demo</p>`;
|
||||
const resolvedTemplate = await resolveTemplateFactory(() => template);
|
||||
expect(resolvedTemplate).toEqual(template);
|
||||
});
|
||||
|
||||
tap.test('resolveTemplateFactory awaits async TemplateResult', async () => {
|
||||
const template = html`<p>async demo</p>`;
|
||||
const resolvedTemplate = await resolveTemplateFactory(async () => {
|
||||
await waitFor(5);
|
||||
return template;
|
||||
});
|
||||
expect(resolvedTemplate).toEqual(template);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
40
ts_demotools/demotools.ts
Normal file
40
ts_demotools/demotools.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DeesElement, customElement, html, css, property, type TemplateResult } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('dees-demowrapper')
|
||||
export class DeesDemoWrapper extends DeesElement {
|
||||
@property({ attribute: false })
|
||||
accessor runAfterRender: (wrapperElement: DeesDemoWrapper) => void | Promise<void>;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.updateComplete;
|
||||
|
||||
// Wait a bit for slotted content to render
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Check if there are slotted elements and runAfterRender is defined
|
||||
if (this.children.length > 0 && this.runAfterRender) {
|
||||
// Call the runAfterRender function with the wrapper element itself
|
||||
// Note: querySelector/querySelectorAll will work on slotted content
|
||||
// because slotted elements remain in the light DOM as children
|
||||
try {
|
||||
await this.runAfterRender(this);
|
||||
} catch (error) {
|
||||
console.error('Error in runAfterRender:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
ts_demotools/index.ts
Normal file
1
ts_demotools/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './demotools.js';
|
||||
5
ts_demotools/plugins.ts
Normal file
5
ts_demotools/plugins.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as deesElement from '@design.estate/dees-element';
|
||||
|
||||
export {
|
||||
deesElement
|
||||
};
|
||||
147
ts_demotools/readme.md
Normal file
147
ts_demotools/readme.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# @design.estate/dees-wcctools/demotools
|
||||
|
||||
🧪 **Demo Wrapper Utilities** — Enhanced testing tools for web component demos
|
||||
|
||||
## Overview
|
||||
|
||||
The demotools module provides `dees-demowrapper`, a utility component for executing post-render logic in component demos. Perfect for simulating user interactions, setting up test data, or validating component state.
|
||||
|
||||
## Installation
|
||||
|
||||
This module is included with `@design.estate/dees-wcctools`:
|
||||
|
||||
```bash
|
||||
pnpm add -D @design.estate/dees-wcctools
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Import the demotools subpath:
|
||||
|
||||
```typescript
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
```
|
||||
|
||||
## DeesDemoWrapper
|
||||
|
||||
A wrapper component that executes a callback after its slotted content renders.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `runAfterRender` | `(wrapper: DeesDemoWrapper) => void \| Promise<void>` | Callback executed after content renders |
|
||||
|
||||
### Example: Basic Usage
|
||||
|
||||
```typescript
|
||||
import { html } from 'lit';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${(wrapper) => {
|
||||
const button = wrapper.querySelector('my-button');
|
||||
console.log('Button found:', button);
|
||||
}}>
|
||||
<my-button>Click Me</my-button>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
```
|
||||
|
||||
### Example: Async Operations
|
||||
|
||||
```typescript
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (wrapper) => {
|
||||
const form = wrapper.querySelector('my-form');
|
||||
|
||||
// Wait for component initialization
|
||||
await form.updateComplete;
|
||||
|
||||
// Simulate user input
|
||||
form.values = { name: 'Test User', email: 'test@example.com' };
|
||||
|
||||
// Trigger validation
|
||||
await form.validate();
|
||||
|
||||
console.log('Form state:', form.isValid);
|
||||
}}>
|
||||
<my-form></my-form>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
```
|
||||
|
||||
### Example: Multiple Elements
|
||||
|
||||
```typescript
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${(wrapper) => {
|
||||
// Find all cards
|
||||
const cards = wrapper.querySelectorAll('my-card');
|
||||
console.log(`Found ${cards.length} cards`);
|
||||
|
||||
// Access by index
|
||||
Array.from(wrapper.children).forEach((child, i) => {
|
||||
console.log(`Child ${i}:`, child.tagName);
|
||||
});
|
||||
|
||||
// Add event listeners
|
||||
wrapper.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', () => console.log('Clicked!'));
|
||||
});
|
||||
}}>
|
||||
<my-card title="Card 1"></my-card>
|
||||
<my-card title="Card 2"></my-card>
|
||||
<my-card title="Card 3"></my-card>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
```
|
||||
|
||||
### Example: Component State Manipulation
|
||||
|
||||
```typescript
|
||||
public static demo = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (wrapper) => {
|
||||
const tabs = wrapper.querySelector('my-tabs');
|
||||
|
||||
// Programmatically switch tabs
|
||||
tabs.activeTab = 'settings';
|
||||
await tabs.updateComplete;
|
||||
|
||||
// Verify content updated
|
||||
const content = tabs.shadowRoot.querySelector('.tab-content');
|
||||
console.log('Active content:', content.textContent);
|
||||
}}>
|
||||
<my-tabs>
|
||||
<div slot="home">Home Content</div>
|
||||
<div slot="settings">Settings Content</div>
|
||||
</my-tabs>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The wrapper renders its slot content immediately
|
||||
2. After a brief delay (50ms) to allow slotted content to initialize
|
||||
3. The `runAfterRender` callback is invoked with the wrapper element
|
||||
4. You have full DOM API access to query and manipulate children
|
||||
|
||||
## Key Features
|
||||
|
||||
- 📦 **Light DOM Access** — Slotted elements remain accessible via standard DOM APIs
|
||||
- ⏱️ **Async Support** — Return a Promise for async operations
|
||||
- 🎯 **Full DOM API** — Use `querySelector`, `querySelectorAll`, `children`, etc.
|
||||
- 🛡️ **Error Handling** — Errors in callbacks are caught and logged
|
||||
|
||||
## CSS Behavior
|
||||
|
||||
The wrapper uses `display: contents` so it doesn't affect layout:
|
||||
|
||||
```css
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
```
|
||||
|
||||
This means the wrapper is "invisible" in the layout — its children render as if they were direct children of the wrapper's parent.
|
||||
3
ts_demotools/tspublish.json
Normal file
3
ts_demotools/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 2
|
||||
}
|
||||
8
ts_web/00_commitinfo_data.ts
Normal file
8
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-wcctools',
|
||||
version: '1.3.0',
|
||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||
}
|
||||
@@ -1,35 +1,64 @@
|
||||
import { LitElement, property, html, customElement, TemplateResult } from 'lit-element';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
||||
import { resolveTemplateFactory } from './wcctools.helpers.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
|
||||
import * as plugins from '../wcctools.plugins';
|
||||
|
||||
import { WccDefaultElement } from './wcc-defaultelement';
|
||||
import * as plugins from '../wcctools.plugins.js';
|
||||
|
||||
// wcc tools
|
||||
import './wcc-frame';
|
||||
import './wcc-sidebar';
|
||||
import './wcc-properties';
|
||||
import './wcc-frame.js';
|
||||
import './wcc-sidebar.js';
|
||||
import './wcc-properties.js';
|
||||
import { type TTheme } from './wcc-properties.js';
|
||||
import { type TElementType } from './wcc-sidebar.js';
|
||||
import { breakpoints } from '@design.estate/dees-domtools';
|
||||
import { WccFrame } from './wcc-frame.js';
|
||||
|
||||
@customElement('wcc-dashboard')
|
||||
export class WccDashboard extends LitElement {
|
||||
@property()
|
||||
public selectedItem: TemplateResult | LitElement;
|
||||
export class WccDashboard extends DeesElement {
|
||||
|
||||
@property()
|
||||
public selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
|
||||
accessor selectedType: TElementType;
|
||||
|
||||
@property()
|
||||
public pages: { [key: string]: TemplateResult } = {};
|
||||
accessor selectedItemName: string;
|
||||
|
||||
@property()
|
||||
public elements: { [key: string]: LitElement } = {};
|
||||
accessor selectedItem: TTemplateFactory | DeesElement;
|
||||
|
||||
@property()
|
||||
public warning: string = null;
|
||||
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
|
||||
|
||||
constructor(elementsArg?: { [key: string]: LitElement }, pagesArg?: { [key: string]: TemplateResult }) {
|
||||
@property()
|
||||
accessor selectedTheme: TTheme = 'dark';
|
||||
|
||||
@property()
|
||||
accessor isFullscreen: boolean = false;
|
||||
|
||||
@property()
|
||||
accessor pages: Record<string, TTemplateFactory> = {};
|
||||
|
||||
@property()
|
||||
accessor elements: { [key: string]: DeesElement } = {};
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
|
||||
private frameScrollY: number = 0;
|
||||
private sidebarScrollY: number = 0;
|
||||
private scrollPositionsApplied: boolean = false;
|
||||
|
||||
@queryAsync('wcc-frame')
|
||||
accessor wccFrame: Promise<WccFrame>;
|
||||
|
||||
constructor(
|
||||
elementsArg?: { [key: string]: DeesElement },
|
||||
pagesArg?: Record<string, TTemplateFactory>
|
||||
) {
|
||||
super();
|
||||
if (elementsArg) {
|
||||
this.elements = elementsArg;
|
||||
console.log('got elements:');
|
||||
console.log(this.elements);
|
||||
}
|
||||
|
||||
if (pagesArg) {
|
||||
@@ -40,9 +69,7 @@ export class WccDashboard extends LitElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto');
|
||||
:host {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: #fcfcfc;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
@@ -51,51 +78,250 @@ export class WccDashboard extends LitElement {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<wcc-sidebar .pages=${this.pages} .elements=${this.elements} @selectedItem=${eventArg => {
|
||||
<wcc-sidebar
|
||||
.dashboardRef=${this}
|
||||
.selectedItem=${this.selectedItem}
|
||||
.isFullscreen=${this.isFullscreen}
|
||||
@selectedType=${(eventArg) => {
|
||||
this.selectedType = eventArg.detail;
|
||||
}}
|
||||
@selectedItemName=${(eventArg) => {
|
||||
this.selectedItemName = eventArg.detail;
|
||||
}}
|
||||
@selectedItem=${(eventArg) => {
|
||||
this.selectedItem = eventArg.detail;
|
||||
}}></wcc-sidebar>
|
||||
<wcc-properties .warning="${this.warning}" .selectedItem=${this.selectedItem} @selectedViewport=${eventArg => {this.selectedViewport = eventArg.detail; this.updateSlot();}}></wcc-properties>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport}>
|
||||
${(() => {
|
||||
if (this.selectedItem instanceof TemplateResult) {
|
||||
return this.selectedItem;
|
||||
} else if (this.selectedItem) {
|
||||
console.log(this.selectedItem);
|
||||
const anonItem: any = this.selectedItem;
|
||||
if (!anonItem.demo) {
|
||||
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
|
||||
return;
|
||||
}}
|
||||
></wcc-sidebar>
|
||||
<wcc-properties
|
||||
.dashboardRef=${this}
|
||||
.warning="${this.warning}"
|
||||
.selectedItem=${this.selectedItem}
|
||||
.selectedViewport=${this.selectedViewport}
|
||||
.selectedTheme=${this.selectedTheme}
|
||||
.isFullscreen=${this.isFullscreen}
|
||||
@selectedViewport=${(eventArg) => {
|
||||
this.selectedViewport = eventArg.detail;
|
||||
this.scheduleUpdate();
|
||||
}}
|
||||
@selectedTheme=${(eventArg) => {
|
||||
this.selectedTheme = eventArg.detail;
|
||||
}}
|
||||
@editorStateChanged=${async (eventArg) => {
|
||||
const frame = await this.wccFrame;
|
||||
if (frame) {
|
||||
frame.advancedEditorOpen = eventArg.detail.isOpen;
|
||||
frame.requestUpdate();
|
||||
}
|
||||
if (!(typeof anonItem.demo === 'function')) {
|
||||
this.setWarning(`component ${anonItem.name} has demo property, but it is not of type function`);
|
||||
return;
|
||||
}
|
||||
this.setWarning(null);
|
||||
return html`${anonItem.demo()}`;
|
||||
} else {
|
||||
this.selectedItem = WccDefaultElement as any;
|
||||
this.updateSlot();
|
||||
}
|
||||
})()}
|
||||
}}
|
||||
@toggleFullscreen=${() => {
|
||||
this.toggleFullscreen();
|
||||
}}
|
||||
></wcc-properties>
|
||||
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isFullscreen=${this.isFullscreen}>
|
||||
</wcc-frame>
|
||||
`;
|
||||
}
|
||||
|
||||
public async updateSlot() {
|
||||
console.log('updateSlot');
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
domtools.setVirtualViewport(this.selectedViewport);
|
||||
await plugins.smartdelay.delayFor(0);
|
||||
super.performUpdate();
|
||||
}
|
||||
|
||||
public setWarning(warningTextArg: string) {
|
||||
if (this.warning !== warningTextArg) {
|
||||
console.log(warningTextArg);
|
||||
this.warning = warningTextArg;
|
||||
setTimeout(() => {
|
||||
super.performUpdate();
|
||||
this.scheduleUpdate();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public toggleFullscreen() {
|
||||
this.isFullscreen = !this.isFullscreen;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
|
||||
// Add ESC key handler for fullscreen mode
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && this.isFullscreen) {
|
||||
this.isFullscreen = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up scroll listeners after DOM is ready
|
||||
setTimeout(() => {
|
||||
this.setupScrollListeners();
|
||||
}, 500);
|
||||
|
||||
this.domtools.router.on(
|
||||
'/wcctools-route/:itemType/:itemName/:viewport/:theme',
|
||||
async (routeInfo) => {
|
||||
this.selectedType = routeInfo.params.itemType as TElementType;
|
||||
this.selectedItemName = routeInfo.params.itemName;
|
||||
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
||||
this.selectedTheme = routeInfo.params.theme as TTheme;
|
||||
if (routeInfo.params.itemType === 'element') {
|
||||
this.selectedItem = this.elements[routeInfo.params.itemName];
|
||||
} else if (routeInfo.params.itemType === 'page') {
|
||||
this.selectedItem = this.pages[routeInfo.params.itemName];
|
||||
}
|
||||
|
||||
// Restore scroll positions from query parameters
|
||||
if (routeInfo.queryParams) {
|
||||
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
||||
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
||||
|
||||
if (frameScrollY) {
|
||||
this.frameScrollY = parseInt(frameScrollY);
|
||||
}
|
||||
if (sidebarScrollY) {
|
||||
this.sidebarScrollY = parseInt(sidebarScrollY);
|
||||
}
|
||||
|
||||
// Apply scroll positions after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
this.applyScrollPositions();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
||||
this.selectedTheme === 'bright'
|
||||
? domtoolsInstance.themeManager.goBright()
|
||||
: domtoolsInstance.themeManager.goDark();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async updated(changedPropertiesArg: Map<string, any>) {
|
||||
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
await this.domtools.router._handleRouteState();
|
||||
const wccFrame: WccFrame = this.shadowRoot.querySelector('wcc-frame');
|
||||
|
||||
if (changedPropertiesArg.has('selectedItemName')) {
|
||||
document.title = this.selectedItemName;
|
||||
};
|
||||
|
||||
if (this.selectedType === 'page' && this.selectedItem) {
|
||||
if (typeof this.selectedItem === 'function') {
|
||||
console.log('slotting page.');
|
||||
const viewport = await wccFrame.getViewportElement();
|
||||
const pageFactory = this.selectedItem as TTemplateFactory;
|
||||
const pageTemplate = await resolveTemplateFactory(pageFactory);
|
||||
render(pageTemplate, viewport);
|
||||
console.log('rendered page.');
|
||||
} else {
|
||||
console.error('The selected item looks strange:');
|
||||
console.log(this.selectedItem);
|
||||
}
|
||||
} else if (this.selectedType === 'element' && this.selectedItem) {
|
||||
console.log('slotting element.');
|
||||
const anonItem: any = this.selectedItem;
|
||||
if (!anonItem.demo) {
|
||||
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
|
||||
return;
|
||||
}
|
||||
if (!(typeof anonItem.demo === 'function')) {
|
||||
this.setWarning(
|
||||
`component ${anonItem.name} has demo property, but it is not of type function`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.setWarning(null);
|
||||
const viewport = await wccFrame.getViewportElement();
|
||||
const demoTemplate = await resolveTemplateFactory(() => anonItem.demo());
|
||||
render(demoTemplate, viewport);
|
||||
}
|
||||
}
|
||||
|
||||
public buildUrl() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
this.domtools.router.pushUrl(fullUrl);
|
||||
}
|
||||
|
||||
private scrollUpdateTimeout: NodeJS.Timeout;
|
||||
private scrollListenersAttached: boolean = false;
|
||||
|
||||
public async setupScrollListeners() {
|
||||
// Prevent duplicate listeners
|
||||
if (this.scrollListenersAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wccFrame = await this.wccFrame;
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
||||
|
||||
if (wccFrame) {
|
||||
// The frame element itself is the scrollable container
|
||||
wccFrame.addEventListener('scroll', () => {
|
||||
this.frameScrollY = wccFrame.scrollTop;
|
||||
this.debouncedScrollUpdate();
|
||||
});
|
||||
this.scrollListenersAttached = true;
|
||||
}
|
||||
|
||||
if (wccSidebar) {
|
||||
// The sidebar element itself is the scrollable container
|
||||
wccSidebar.addEventListener('scroll', () => {
|
||||
this.sidebarScrollY = wccSidebar.scrollTop;
|
||||
this.debouncedScrollUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private debouncedScrollUpdate() {
|
||||
clearTimeout(this.scrollUpdateTimeout);
|
||||
this.scrollUpdateTimeout = setTimeout(() => {
|
||||
this.updateUrlWithScrollState();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private updateUrlWithScrollState() {
|
||||
const baseUrl = `/wcctools-route/${this.selectedType}/${this.selectedItemName}/${this.selectedViewport}/${this.selectedTheme}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (this.frameScrollY > 0) {
|
||||
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
||||
}
|
||||
if (this.sidebarScrollY > 0) {
|
||||
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
// Use replaceState to update URL without navigation
|
||||
window.history.replaceState(null, '', fullUrl);
|
||||
}
|
||||
|
||||
public async applyScrollPositions() {
|
||||
// Only apply scroll positions once to avoid interfering with user scrolling
|
||||
if (this.scrollPositionsApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wccFrame = await this.wccFrame;
|
||||
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
||||
|
||||
if (wccFrame && this.frameScrollY > 0) {
|
||||
// The frame element itself is the scrollable container
|
||||
wccFrame.scrollTop = this.frameScrollY;
|
||||
}
|
||||
|
||||
if (wccSidebar && this.sidebarScrollY > 0) {
|
||||
// The sidebar element itself is the scrollable container
|
||||
wccSidebar.scrollTop = this.sidebarScrollY;
|
||||
}
|
||||
|
||||
this.scrollPositionsApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { LitElement, property, html, customElement } from 'lit-element';
|
||||
import { TemplateResult } from 'lit-html';
|
||||
|
||||
@customElement('wcc-defaultelement')
|
||||
export class WccDefaultElement extends LitElement {
|
||||
public static demo = () => html`<wcc-defaultelement></wcc-defaultelement>`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// you have access to all kinds of things through this.
|
||||
// this.setAttribute('gotIt','true');
|
||||
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto');
|
||||
:host {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: #333;
|
||||
text-align: center;
|
||||
padding:30px;
|
||||
box-shadow: 0px 0px 5px rgba(0,0,0,0.6);
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
}
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
No Element specified!
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,64 @@
|
||||
import { LitElement, property, html, customElement, TemplateResult } from 'lit-element';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wcc-frame': WccFrame;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('wcc-frame')
|
||||
export class WccFrame extends LitElement {
|
||||
export class WccFrame extends DeesElement {
|
||||
@property()
|
||||
public viewport: string;
|
||||
accessor viewport: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor advancedEditorOpen: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor isFullscreen: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
border: 10px solid #ffaeaf;
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#333', '#000')};
|
||||
left: 200px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
container-type: inline-size;
|
||||
container-name: wccToolsViewport;
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
`,
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
${this.isFullscreen ? `
|
||||
border: none !important;
|
||||
left: 0px !important;
|
||||
right: 0px !important;
|
||||
top: 0px !important;
|
||||
bottom: 0px !important;
|
||||
` : `
|
||||
bottom: ${this.advancedEditorOpen ? '400px' : '100px'};
|
||||
border: 10px solid #ffaeaf;
|
||||
background: #222;
|
||||
position: absolute;
|
||||
left: 200px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 100px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
${(() => {
|
||||
`}
|
||||
transition: all 0.3s ease;
|
||||
${this.isFullscreen ? 'padding: 0px;' : (() => {
|
||||
switch (this.viewport) {
|
||||
case 'desktop':
|
||||
return `
|
||||
@@ -49,12 +87,22 @@ export class WccFrame extends LitElement {
|
||||
}
|
||||
|
||||
.viewport {
|
||||
position: relative;
|
||||
${this.viewport !== 'desktop'
|
||||
${!this.isFullscreen && this.viewport !== 'desktop'
|
||||
? html` border-right: 1px dotted #444; border-left: 1px dotted #444; `
|
||||
: html``}
|
||||
min-height: 100%;
|
||||
: html``
|
||||
}
|
||||
background:
|
||||
${
|
||||
this.goBright ? `
|
||||
radial-gradient(#CCCCCC 3px, transparent 4px),
|
||||
radial-gradient(#CCCCCC 3px, transparent 4px),
|
||||
linear-gradient(#eeeeee 4px, transparent 0),
|
||||
linear-gradient(45deg, transparent 74px, transparent 75px, #CCCCCC 75px, #CCCCCC 76px, transparent 77px, transparent 109px),
|
||||
linear-gradient(-45deg, transparent 75px, transparent 76px, #CCCCCC 76px, #CCCCCC 77px, transparent 78px, transparent 109px),
|
||||
#eeeeee;
|
||||
background-size: 109px 109px, 109px 109px,100% 6px, 109px 109px, 109px 109px;
|
||||
background-position: 54px 55px, 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
||||
` : `
|
||||
radial-gradient(#444444 3px, transparent 4px),
|
||||
radial-gradient(#444444 3px, transparent 4px),
|
||||
linear-gradient(#222222 4px, transparent 0),
|
||||
@@ -63,11 +111,23 @@ export class WccFrame extends LitElement {
|
||||
#222222;
|
||||
background-size: 109px 109px, 109px 109px,100% 6px, 109px 109px, 109px 109px;
|
||||
background-position: 54px 55px, 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
||||
`
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="viewport">
|
||||
<slot></slot>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async getDisplayedInstance() {
|
||||
await this.updateComplete;
|
||||
const slottedContent = this.children;
|
||||
console.log(slottedContent);
|
||||
}
|
||||
|
||||
public async getViewportElement(): Promise<HTMLElement> {
|
||||
return this.shadowRoot.querySelector('.viewport') as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
108
ts_web/elements/wcc-record-button.ts
Normal file
108
ts_web/elements/wcc-record-button.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { DeesElement, customElement, html, css, property, type TemplateResult } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('wcc-record-button')
|
||||
export class WccRecordButton extends DeesElement {
|
||||
@property({ type: String })
|
||||
accessor state: 'idle' | 'recording' = 'idle';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor duration: number = 0;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:host(:hover) {
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
:host(.recording) {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.rec-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
:host(.recording) .rec-icon {
|
||||
animation: pulse-recording 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-recording {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.9); }
|
||||
}
|
||||
|
||||
.recording-timer {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
private formatDuration(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="rec-icon"></div>
|
||||
${this.state === 'recording' ? html`
|
||||
<span class="recording-timer">${this.formatDuration(this.duration)}</span>
|
||||
` : null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
await super.connectedCallback();
|
||||
this.addEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
this.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
this.dispatchEvent(new CustomEvent('record-click', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
};
|
||||
|
||||
updated(changedProperties: Map<string, unknown>): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('state')) {
|
||||
if (this.state === 'recording') {
|
||||
this.classList.add('recording');
|
||||
} else {
|
||||
this.classList.remove('recording');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
974
ts_web/elements/wcc-recording-panel.ts
Normal file
974
ts_web/elements/wcc-recording-panel.ts
Normal file
@@ -0,0 +1,974 @@
|
||||
import { DeesElement, customElement, html, css, property, state, type TemplateResult } from '@design.estate/dees-element';
|
||||
import { RecorderService } from '../services/recorder.service.js';
|
||||
import type { WccDashboard } from './wcc-dashboard.js';
|
||||
|
||||
@customElement('wcc-recording-panel')
|
||||
export class WccRecordingPanel extends DeesElement {
|
||||
// External configuration
|
||||
@property({ attribute: false })
|
||||
accessor dashboardRef: WccDashboard;
|
||||
|
||||
// Panel state
|
||||
@state()
|
||||
accessor panelState: 'options' | 'recording' | 'preview' = 'options';
|
||||
|
||||
// Recording options
|
||||
@state()
|
||||
accessor recordingMode: 'viewport' | 'screen' = 'viewport';
|
||||
|
||||
@state()
|
||||
accessor audioEnabled: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor selectedMicrophoneId: string = '';
|
||||
|
||||
@state()
|
||||
accessor availableMicrophones: MediaDeviceInfo[] = [];
|
||||
|
||||
@state()
|
||||
accessor audioLevel: number = 0;
|
||||
|
||||
// Recording state
|
||||
@state()
|
||||
accessor recordingDuration: number = 0;
|
||||
|
||||
// Preview/trim state
|
||||
@state()
|
||||
accessor previewVideoUrl: string = '';
|
||||
|
||||
@state()
|
||||
accessor trimStart: number = 0;
|
||||
|
||||
@state()
|
||||
accessor trimEnd: number = 0;
|
||||
|
||||
@state()
|
||||
accessor videoDuration: number = 0;
|
||||
|
||||
@state()
|
||||
accessor isDraggingTrim: 'start' | 'end' | null = null;
|
||||
|
||||
@state()
|
||||
accessor isExporting: boolean = false;
|
||||
|
||||
// Service instance
|
||||
private recorderService: RecorderService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.recorderService = new RecorderService({
|
||||
onDurationUpdate: (duration) => {
|
||||
this.recordingDuration = duration;
|
||||
this.dispatchEvent(new CustomEvent('duration-update', {
|
||||
detail: { duration },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
},
|
||||
onRecordingComplete: (blob) => {
|
||||
this.handleRecordingComplete(blob);
|
||||
},
|
||||
onAudioLevelUpdate: (level) => {
|
||||
this.audioLevel = level;
|
||||
},
|
||||
onStreamEnded: () => {
|
||||
this.stopRecording();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
/* CSS Variables */
|
||||
--background: #0a0a0a;
|
||||
--foreground: #e5e5e5;
|
||||
--input: #141414;
|
||||
--primary: #3b82f6;
|
||||
--border: rgba(255, 255, 255, 0.06);
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 6px;
|
||||
}
|
||||
|
||||
/* Recording Options Panel */
|
||||
.recording-options-panel {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 116px;
|
||||
width: 360px;
|
||||
background: #0c0c0c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.recording-options-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recording-options-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.recording-options-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.recording-options-close:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recording-options-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.recording-option-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.recording-option-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.recording-option-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.recording-mode-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.recording-mode-btn {
|
||||
flex: 1;
|
||||
padding: 0.6rem 0.75rem;
|
||||
background: var(--input);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #999;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recording-mode-btn:hover {
|
||||
border-color: var(--primary);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.recording-mode-btn.selected {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.audio-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.audio-toggle input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.audio-toggle label {
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.microphone-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--input);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--foreground);
|
||||
font-size: 0.75rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.microphone-select:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.microphone-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.audio-level-container {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.audio-level-label {
|
||||
font-size: 0.65rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.audio-level-bar {
|
||||
height: 8px;
|
||||
background: var(--input);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.audio-level-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #22c55e, #84cc16, #eab308);
|
||||
border-radius: 4px;
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
|
||||
.start-recording-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #dc2626;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.start-recording-btn:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.start-recording-btn .rec-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Preview Modal */
|
||||
.preview-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.preview-modal {
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
background: #0c0c0c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.preview-modal-header {
|
||||
padding: 1rem 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-modal-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.preview-modal-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.preview-modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.preview-modal-content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.preview-video-container {
|
||||
background: #000;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.preview-modal-actions {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.preview-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.preview-btn.secondary:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.preview-btn.primary {
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-btn.primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.preview-btn.primary:disabled {
|
||||
background: #1e3a5f;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Trim Timeline Styles */
|
||||
.trim-section {
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.trim-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.trim-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.trim-duration-info {
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.trim-timeline {
|
||||
position: relative;
|
||||
height: 48px;
|
||||
background: var(--input);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 0.75rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.trim-track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
height: 6px;
|
||||
background: #333;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.trim-selected {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
height: 6px;
|
||||
background: var(--primary);
|
||||
transform: translateY(-50%);
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.trim-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 36px;
|
||||
background: white;
|
||||
border: 2px solid var(--primary);
|
||||
border-radius: 4px;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: ew-resize;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.trim-handle:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.trim-handle:active {
|
||||
background: var(--primary);
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
|
||||
.trim-handle::before {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
background: #666;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.trim-handle:active::before {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.trim-time-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.65rem;
|
||||
color: #666;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.trim-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.trim-action-btn {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--input);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #999;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trim-action-btn:hover {
|
||||
border-color: var(--primary);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.export-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.panelState === 'options') {
|
||||
return this.renderOptionsPanel();
|
||||
} else if (this.panelState === 'preview') {
|
||||
return this.renderPreviewModal();
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
private renderOptionsPanel(): TemplateResult {
|
||||
return html`
|
||||
<div class="recording-options-panel">
|
||||
<div class="recording-options-header">
|
||||
<span class="recording-options-title">Recording Settings</span>
|
||||
<button class="recording-options-close" @click=${() => this.close()}>✕</button>
|
||||
</div>
|
||||
<div class="recording-options-content">
|
||||
<div class="recording-option-group">
|
||||
<div class="recording-option-label">Record Area</div>
|
||||
<div class="recording-mode-buttons">
|
||||
<button
|
||||
class="recording-mode-btn ${this.recordingMode === 'viewport' ? 'selected' : ''}"
|
||||
@click=${() => this.recordingMode = 'viewport'}
|
||||
>
|
||||
Viewport Only
|
||||
</button>
|
||||
<button
|
||||
class="recording-mode-btn ${this.recordingMode === 'screen' ? 'selected' : ''}"
|
||||
@click=${() => this.recordingMode = 'screen'}
|
||||
>
|
||||
Entire Screen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recording-option-group">
|
||||
<div class="recording-option-label">Audio</div>
|
||||
<div class="audio-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="audioToggle"
|
||||
?checked=${this.audioEnabled}
|
||||
@change=${(e: Event) => this.handleAudioToggle((e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<label for="audioToggle">Enable Microphone</label>
|
||||
</div>
|
||||
|
||||
${this.audioEnabled ? html`
|
||||
<select
|
||||
class="microphone-select"
|
||||
.value=${this.selectedMicrophoneId}
|
||||
@change=${(e: Event) => this.handleMicrophoneChange((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">Select Microphone...</option>
|
||||
${this.availableMicrophones.map(mic => html`
|
||||
<option value=${mic.deviceId}>${mic.label || `Microphone ${mic.deviceId.slice(0, 8)}`}</option>
|
||||
`)}
|
||||
</select>
|
||||
|
||||
${this.selectedMicrophoneId ? html`
|
||||
<div class="audio-level-container">
|
||||
<div class="audio-level-label">Input Level</div>
|
||||
<div class="audio-level-bar">
|
||||
<div class="audio-level-fill" style="width: ${this.audioLevel}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
` : null}
|
||||
` : null}
|
||||
</div>
|
||||
|
||||
<button class="start-recording-btn" @click=${() => this.startRecording()}>
|
||||
<div class="rec-dot"></div>
|
||||
Start Recording
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPreviewModal(): TemplateResult {
|
||||
return html`
|
||||
<div class="preview-modal-overlay" @click=${(e: Event) => {
|
||||
if ((e.target as HTMLElement).classList.contains('preview-modal-overlay')) {
|
||||
this.discardRecording();
|
||||
}
|
||||
}}>
|
||||
<div class="preview-modal">
|
||||
<div class="preview-modal-header">
|
||||
<span class="preview-modal-title">Recording Preview</span>
|
||||
<button class="preview-modal-close" @click=${() => this.discardRecording()}>✕</button>
|
||||
</div>
|
||||
<div class="preview-modal-content">
|
||||
<div class="preview-video-container">
|
||||
<video
|
||||
class="preview-video"
|
||||
src=${this.previewVideoUrl}
|
||||
controls
|
||||
@loadedmetadata=${(e: Event) => this.handleVideoLoaded(e.target as HTMLVideoElement)}
|
||||
></video>
|
||||
</div>
|
||||
|
||||
<!-- Trim Section -->
|
||||
<div class="trim-section">
|
||||
<div class="trim-section-header">
|
||||
<span class="trim-section-title">Trim Video</span>
|
||||
<span class="trim-duration-info">
|
||||
${this.formatDuration(Math.floor(this.trimEnd - this.trimStart))}
|
||||
${this.trimStart > 0 || this.trimEnd < this.videoDuration
|
||||
? `(trimmed from ${this.formatDuration(Math.floor(this.videoDuration))})`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="trim-timeline"
|
||||
@mousedown=${(e: MouseEvent) => this.handleTimelineClick(e)}
|
||||
@mousemove=${(e: MouseEvent) => this.handleTimelineDrag(e)}
|
||||
@mouseup=${() => this.handleTimelineDragEnd()}
|
||||
@mouseleave=${() => this.handleTimelineDragEnd()}
|
||||
>
|
||||
<div class="trim-track"></div>
|
||||
<div
|
||||
class="trim-selected"
|
||||
style="left: ${this.getHandlePositionStyle(this.trimStart)}; right: ${this.getHandlePositionFromEndStyle(this.trimEnd)};"
|
||||
></div>
|
||||
<div
|
||||
class="trim-handle start-handle"
|
||||
style="left: ${this.getHandlePositionStyle(this.trimStart)};"
|
||||
@mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'start'; }}
|
||||
></div>
|
||||
<div
|
||||
class="trim-handle end-handle"
|
||||
style="left: ${this.getHandlePositionStyle(this.trimEnd)};"
|
||||
@mousedown=${(e: MouseEvent) => { e.stopPropagation(); this.isDraggingTrim = 'end'; }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="trim-time-labels">
|
||||
<span>${this.formatDuration(Math.floor(this.trimStart))}</span>
|
||||
<span>${this.formatDuration(Math.floor(this.trimEnd))}</span>
|
||||
</div>
|
||||
|
||||
<div class="trim-actions">
|
||||
<button class="trim-action-btn" @click=${() => this.resetTrim()}>
|
||||
Reset Trim
|
||||
</button>
|
||||
<button class="trim-action-btn" @click=${() => this.previewTrimmedSection()}>
|
||||
Preview Selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-modal-actions">
|
||||
<button class="preview-btn secondary" @click=${() => this.discardRecording()}>Discard</button>
|
||||
<button
|
||||
class="preview-btn primary"
|
||||
?disabled=${this.isExporting}
|
||||
@click=${() => this.downloadRecording()}
|
||||
>
|
||||
${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==================== Audio Methods ====================
|
||||
|
||||
private async handleAudioToggle(enabled: boolean): Promise<void> {
|
||||
this.audioEnabled = enabled;
|
||||
if (enabled) {
|
||||
this.availableMicrophones = await this.recorderService.loadMicrophones(true);
|
||||
if (this.availableMicrophones.length > 0 && !this.selectedMicrophoneId) {
|
||||
this.selectedMicrophoneId = this.availableMicrophones[0].deviceId;
|
||||
await this.recorderService.startAudioMonitoring(this.selectedMicrophoneId);
|
||||
}
|
||||
} else {
|
||||
this.recorderService.stopAudioMonitoring();
|
||||
this.selectedMicrophoneId = '';
|
||||
this.audioLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMicrophoneChange(deviceId: string): Promise<void> {
|
||||
this.selectedMicrophoneId = deviceId;
|
||||
if (deviceId) {
|
||||
await this.recorderService.startAudioMonitoring(deviceId);
|
||||
} else {
|
||||
this.recorderService.stopAudioMonitoring();
|
||||
this.audioLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Recording Methods ====================
|
||||
|
||||
private async startRecording(): Promise<void> {
|
||||
try {
|
||||
let viewportElement: HTMLElement | undefined;
|
||||
if (this.recordingMode === 'viewport' && this.dashboardRef) {
|
||||
const wccFrame = await this.dashboardRef.wccFrame;
|
||||
viewportElement = await wccFrame.getViewportElement();
|
||||
}
|
||||
|
||||
await this.recorderService.startRecording({
|
||||
mode: this.recordingMode,
|
||||
audioDeviceId: this.audioEnabled ? this.selectedMicrophoneId : undefined,
|
||||
viewportElement
|
||||
});
|
||||
|
||||
this.panelState = 'recording';
|
||||
this.dispatchEvent(new CustomEvent('recording-start', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to start recording:', error);
|
||||
this.panelState = 'options';
|
||||
}
|
||||
}
|
||||
|
||||
public stopRecording(): void {
|
||||
this.recorderService.stopRecording();
|
||||
}
|
||||
|
||||
private handleRecordingComplete(blob: Blob): void {
|
||||
if (this.previewVideoUrl) {
|
||||
URL.revokeObjectURL(this.previewVideoUrl);
|
||||
}
|
||||
this.previewVideoUrl = URL.createObjectURL(blob);
|
||||
this.panelState = 'preview';
|
||||
this.dispatchEvent(new CustomEvent('recording-stop', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private discardRecording(): void {
|
||||
if (this.previewVideoUrl) {
|
||||
URL.revokeObjectURL(this.previewVideoUrl);
|
||||
this.previewVideoUrl = '';
|
||||
}
|
||||
this.recorderService.reset();
|
||||
this.trimStart = 0;
|
||||
this.trimEnd = 0;
|
||||
this.videoDuration = 0;
|
||||
this.isExporting = false;
|
||||
this.recordingDuration = 0;
|
||||
this.close();
|
||||
}
|
||||
|
||||
private async downloadRecording(): Promise<void> {
|
||||
const recordedBlob = this.recorderService.recordedBlob;
|
||||
if (!recordedBlob) return;
|
||||
|
||||
this.isExporting = true;
|
||||
|
||||
try {
|
||||
let blobToDownload: Blob;
|
||||
|
||||
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
||||
|
||||
if (needsTrim) {
|
||||
const video = this.shadowRoot?.querySelector('.preview-video') as HTMLVideoElement;
|
||||
if (video) {
|
||||
blobToDownload = await this.recorderService.exportTrimmedVideo(video, this.trimStart, this.trimEnd);
|
||||
} else {
|
||||
blobToDownload = recordedBlob;
|
||||
}
|
||||
} else {
|
||||
blobToDownload = recordedBlob;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const filename = `wcctools-recording-${timestamp}.webm`;
|
||||
|
||||
const url = URL.createObjectURL(blobToDownload);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.discardRecording();
|
||||
} catch (error) {
|
||||
console.error('Error exporting video:', error);
|
||||
this.isExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Trim Methods ====================
|
||||
|
||||
private handleVideoLoaded(video: HTMLVideoElement): void {
|
||||
// WebM files from MediaRecorder may have Infinity/NaN duration
|
||||
// Fall back to the tracked recording duration
|
||||
const duration = Number.isFinite(video.duration) ? video.duration : this.recordingDuration;
|
||||
this.videoDuration = duration;
|
||||
this.trimStart = 0;
|
||||
this.trimEnd = duration;
|
||||
}
|
||||
|
||||
private formatDuration(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
private getHandlePositionStyle(time: number): string {
|
||||
if (this.videoDuration === 0) return '12px';
|
||||
const percentage = time / this.videoDuration;
|
||||
// Formula: 12px padding + percentage of remaining width (total - 24px padding)
|
||||
// At 0%: 12px (left edge of track)
|
||||
// At 100%: calc(100% - 12px) (right edge of track)
|
||||
return `calc(12px + ${(percentage * 100).toFixed(2)}% - ${(percentage * 24).toFixed(2)}px)`;
|
||||
}
|
||||
|
||||
private getHandlePositionFromEndStyle(time: number): string {
|
||||
if (this.videoDuration === 0) return '12px';
|
||||
const percentage = time / this.videoDuration;
|
||||
const remainingPercentage = 1 - percentage;
|
||||
// For CSS 'right' property: distance from right edge
|
||||
// At trimEnd = 100%: right = 12px (at right edge of track)
|
||||
// At trimEnd = 0%: right = calc(100% - 12px) (at left edge of track)
|
||||
return `calc(12px + ${(remainingPercentage * 100).toFixed(2)}% - ${(remainingPercentage * 24).toFixed(2)}px)`;
|
||||
}
|
||||
|
||||
private handleTimelineClick(e: MouseEvent): void {
|
||||
if (this.isDraggingTrim) return;
|
||||
|
||||
const timeline = e.currentTarget as HTMLElement;
|
||||
const rect = timeline.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(1, (x - 12) / (rect.width - 24)));
|
||||
const time = percentage * this.videoDuration;
|
||||
|
||||
const video = this.shadowRoot?.querySelector('.preview-video') as HTMLVideoElement;
|
||||
if (video) {
|
||||
video.currentTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
private handleTimelineDrag(e: MouseEvent): void {
|
||||
if (!this.isDraggingTrim) return;
|
||||
|
||||
const timeline = e.currentTarget as HTMLElement;
|
||||
const rect = timeline.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(1, (x - 12) / (rect.width - 24)));
|
||||
const time = percentage * this.videoDuration;
|
||||
|
||||
const minDuration = 1;
|
||||
|
||||
if (this.isDraggingTrim === 'start') {
|
||||
this.trimStart = Math.min(time, this.trimEnd - minDuration);
|
||||
this.trimStart = Math.max(0, this.trimStart);
|
||||
} else if (this.isDraggingTrim === 'end') {
|
||||
this.trimEnd = Math.max(time, this.trimStart + minDuration);
|
||||
this.trimEnd = Math.min(this.videoDuration, this.trimEnd);
|
||||
}
|
||||
|
||||
const video = this.shadowRoot?.querySelector('.preview-video') as HTMLVideoElement;
|
||||
if (video) {
|
||||
video.currentTime = this.isDraggingTrim === 'start' ? this.trimStart : this.trimEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private handleTimelineDragEnd(): void {
|
||||
this.isDraggingTrim = null;
|
||||
}
|
||||
|
||||
private resetTrim(): void {
|
||||
this.trimStart = 0;
|
||||
this.trimEnd = this.videoDuration;
|
||||
|
||||
const video = this.shadowRoot?.querySelector('.preview-video') as HTMLVideoElement;
|
||||
if (video) {
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private previewTrimmedSection(): void {
|
||||
const video = this.shadowRoot?.querySelector('.preview-video') as HTMLVideoElement;
|
||||
if (!video) return;
|
||||
|
||||
video.currentTime = this.trimStart;
|
||||
video.play();
|
||||
|
||||
const checkTime = () => {
|
||||
if (video.currentTime >= this.trimEnd) {
|
||||
video.pause();
|
||||
video.removeEventListener('timeupdate', checkTime);
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener('timeupdate', checkTime);
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
private close(): void {
|
||||
this.recorderService.stopAudioMonitoring();
|
||||
this.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
this.recorderService.dispose();
|
||||
if (this.previewVideoUrl) {
|
||||
URL.revokeObjectURL(this.previewVideoUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,180 @@
|
||||
import { LitElement, property, html, customElement, TemplateResult } from 'lit-element';
|
||||
import * as plugins from '../wcctools.plugins.js';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
|
||||
import { WccDashboard } from './wcc-dashboard.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
|
||||
export type TElementType = 'element' | 'page';
|
||||
|
||||
@customElement('wcc-sidebar')
|
||||
export class WccSidebar extends LitElement {
|
||||
@property({type: Array})
|
||||
public websites: string[] = [];
|
||||
|
||||
@property()
|
||||
public pages: { [key: string]: TemplateResult } = {};
|
||||
|
||||
@property()
|
||||
public elements: { [key: string]: LitElement } = {};
|
||||
export class WccSidebar extends DeesElement {
|
||||
@property({ attribute: false })
|
||||
accessor selectedItem: DeesElement | TTemplateFactory;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selectedItem: LitElement | TemplateResult;
|
||||
accessor selectedType: TElementType;
|
||||
|
||||
@property()
|
||||
accessor dashboardRef: WccDashboard;
|
||||
|
||||
@property()
|
||||
accessor isFullscreen: boolean = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
border-right: 1px solid #999;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
/* CSS Variables - Always dark theme to match wcc-properties */
|
||||
--background: #0a0a0a;
|
||||
--foreground: #e5e5e5;
|
||||
--card: #0f0f0f;
|
||||
--card-foreground: #f0f0f0;
|
||||
--muted: #1a1a1a;
|
||||
--muted-foreground: #666;
|
||||
--accent: #222;
|
||||
--accent-foreground: #fff;
|
||||
--border: rgba(255, 255, 255, 0.06);
|
||||
--input: #141414;
|
||||
--primary: #3b82f6;
|
||||
--primary-foreground: #fff;
|
||||
--ring: #3b82f6;
|
||||
--radius: 4px;
|
||||
|
||||
display: ${this.isFullscreen ? 'none' : 'block'};
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin: 20px 5px 5px 5px;
|
||||
.menu {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.subheading {
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
padding: 0.3rem 0.75rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #888;
|
||||
margin: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: rgba(59, 130, 246, 0.03);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectOption {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
line-height: 24px;
|
||||
padding: 5px;
|
||||
transition: all 0.2s;
|
||||
margin: 0.125rem 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: all 0.15s ease;
|
||||
display: grid;
|
||||
grid-template-columns: 28px auto;
|
||||
}
|
||||
.selectOption:hover {
|
||||
grid-template-columns: 20px 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.selectOption:hover {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.selectOption:hover .material-symbols-outlined {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.selectOption.selected {
|
||||
background: #455A64;;
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.selectOption.selected .material-symbols-outlined {
|
||||
opacity: 1;
|
||||
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
|
||||
.selectOption.selected:hover {
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
background: #455A64;
|
||||
}
|
||||
|
||||
.selectOption .material-icons {
|
||||
color: #666;
|
||||
display: block;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.selectOption.selected .material-icons {
|
||||
color: #000;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.selectOption .text {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
<div class="heading">
|
||||
lele-catalog
|
||||
</div>
|
||||
<div class="subheading">
|
||||
Lossless GmbH
|
||||
</div>
|
||||
<div class="menu">
|
||||
<h3>Live Websites</h3>
|
||||
${this.websites.map(website => {
|
||||
return html`<div class="selectOption"><i class="material-icons">ondemand_video</i><div class="text">${website}</div></div>`;
|
||||
})}
|
||||
<h3>Pages</h3>
|
||||
${(() => {
|
||||
const pages = Object.keys(this.pages);
|
||||
const pages = Object.keys(this.dashboardRef.pages);
|
||||
return pages.map(pageName => {
|
||||
const item = this.pages[pageName];
|
||||
const item = this.dashboardRef.pages[pageName];
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : console.log('hi')}"
|
||||
@click=${() => {
|
||||
this.selectItem(item);
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
||||
@click=${async () => {
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('page', pageName, item);
|
||||
}}
|
||||
>
|
||||
<i class="material-icons">insert_drive_file</i>
|
||||
<i class="material-symbols-outlined">insert_drive_file</i>
|
||||
<div class="text">${pageName}</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -118,33 +182,49 @@ export class WccSidebar extends LitElement {
|
||||
})()}
|
||||
<h3>Elements</h3>
|
||||
${(() => {
|
||||
const elements = Object.keys(this.elements);
|
||||
const elements = Object.keys(this.dashboardRef.elements);
|
||||
return elements.map(elementName => {
|
||||
const item = this.elements[elementName];
|
||||
const item = this.dashboardRef.elements[elementName];
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : console.log('hi')}"
|
||||
@click=${() => {
|
||||
this.selectItem(item);
|
||||
class="selectOption ${this.selectedItem === item ? 'selected' : null}"
|
||||
@click=${async () => {
|
||||
const domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
||||
this.selectItem('element', elementName, item);
|
||||
}}
|
||||
>
|
||||
<i class="material-icons">featured_video</i>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
<div class="text">${elementName}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
})()}
|
||||
</menu>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public selectItem(item: TemplateResult | LitElement) {
|
||||
public selectItem(typeArg: TElementType, itemNameArg: string, itemArg: TTemplateFactory | DeesElement) {
|
||||
console.log('selected item');
|
||||
this.selectedItem = item;
|
||||
console.log(itemNameArg);
|
||||
console.log(itemArg);
|
||||
this.selectedItem = itemArg;
|
||||
this.selectedType = typeArg;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedItem', {
|
||||
detail: item
|
||||
new CustomEvent('selectedType', {
|
||||
detail: typeArg
|
||||
})
|
||||
);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedItemName', {
|
||||
detail: itemNameArg
|
||||
})
|
||||
);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedItem', {
|
||||
detail: itemArg
|
||||
})
|
||||
);
|
||||
|
||||
this.dashboardRef.buildUrl();
|
||||
}
|
||||
}
|
||||
|
||||
9
ts_web/elements/wcctools.helpers.ts
Normal file
9
ts_web/elements/wcctools.helpers.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export type TTemplateFactory = () => TemplateResult | Promise<TemplateResult>;
|
||||
|
||||
export const resolveTemplateFactory = async (
|
||||
factoryArg: TTemplateFactory
|
||||
): Promise<TemplateResult> => {
|
||||
return await Promise.resolve(factoryArg());
|
||||
};
|
||||
@@ -1,7 +1,16 @@
|
||||
import { WccDashboard } from './elements/wcc-dashboard';
|
||||
import { LitElement, TemplateResult } from 'lit-element';
|
||||
import { WccDashboard } from './elements/wcc-dashboard.js';
|
||||
import { LitElement } from 'lit';
|
||||
import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
||||
|
||||
const setupWccTools = (elementsArg?: { [key: string]: LitElement }, pagesArg?: { [key: string]: TemplateResult }) => {
|
||||
// Export recording components and service
|
||||
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
||||
export { WccRecordButton } from './elements/wcc-record-button.js';
|
||||
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
||||
|
||||
const setupWccTools = (
|
||||
elementsArg?: { [key: string]: LitElement },
|
||||
pagesArg?: Record<string, TTemplateFactory>
|
||||
) => {
|
||||
let hasRun = false;
|
||||
const runWccToolsSetup = async () => {
|
||||
if (document.readyState === 'complete' && !hasRun) {
|
||||
|
||||
1
ts_web/pages/index.ts
Normal file
1
ts_web/pages/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const page1 = null;
|
||||
123
ts_web/readme.md
Normal file
123
ts_web/readme.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# @design.estate/dees-wcctools
|
||||
|
||||
🛠️ **Web Component Catalogue Tools** — The core dashboard and UI components for building interactive component catalogues
|
||||
|
||||
## Overview
|
||||
|
||||
This is the main module of `@design.estate/dees-wcctools`, providing the complete dashboard experience for developing, testing, and documenting web components.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add -D @design.estate/dees-wcctools
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { setupWccTools } from '@design.estate/dees-wcctools';
|
||||
import { MyButton } from './components/my-button.js';
|
||||
|
||||
setupWccTools({
|
||||
'my-button': MyButton,
|
||||
});
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
### Main Entry Point
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `setupWccTools` | Initialize the component catalogue dashboard |
|
||||
|
||||
### Recording Components
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `RecorderService` | Service class for screen/viewport recording |
|
||||
| `WccRecordButton` | Record button UI component |
|
||||
| `WccRecordingPanel` | Recording options and preview panel |
|
||||
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
||||
| `IRecordingOptions` | TypeScript interface for recording options |
|
||||
|
||||
## Internal Components
|
||||
|
||||
The module includes these internal web components:
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `wcc-dashboard` | Main dashboard container with routing |
|
||||
| `wcc-sidebar` | Navigation sidebar with element/page listing |
|
||||
| `wcc-frame` | Iframe viewport with responsive sizing |
|
||||
| `wcc-properties` | Property panel with live editing |
|
||||
| `wcc-record-button` | Recording state indicator button |
|
||||
| `wcc-recording-panel` | Recording workflow UI |
|
||||
|
||||
## RecorderService API
|
||||
|
||||
For programmatic recording control:
|
||||
|
||||
```typescript
|
||||
import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
|
||||
|
||||
const events: IRecorderEvents = {
|
||||
onDurationUpdate: (duration) => console.log(`Recording: ${duration}s`),
|
||||
onRecordingComplete: (blob) => saveBlob(blob),
|
||||
onAudioLevelUpdate: (level) => updateMeter(level),
|
||||
onError: (error) => console.error(error),
|
||||
onStreamEnded: () => console.log('User stopped sharing'),
|
||||
};
|
||||
|
||||
const recorder = new RecorderService(events);
|
||||
|
||||
// Load available microphones
|
||||
const mics = await recorder.loadMicrophones(true); // true = request permission
|
||||
|
||||
// Start audio level monitoring
|
||||
await recorder.startAudioMonitoring(mics[0].deviceId);
|
||||
|
||||
// Start recording
|
||||
await recorder.startRecording({
|
||||
mode: 'viewport', // or 'screen'
|
||||
audioDeviceId: mics[0].deviceId,
|
||||
viewportElement: document.querySelector('.viewport'),
|
||||
});
|
||||
|
||||
// Stop recording
|
||||
recorder.stopRecording();
|
||||
|
||||
// Export trimmed video
|
||||
const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
|
||||
|
||||
// Cleanup
|
||||
recorder.dispose();
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ts_web/
|
||||
├── index.ts # Main exports
|
||||
├── elements/
|
||||
│ ├── wcc-dashboard.ts # Root dashboard component
|
||||
│ ├── wcc-sidebar.ts # Navigation sidebar
|
||||
│ ├── wcc-frame.ts # Responsive iframe viewport
|
||||
│ ├── wcc-properties.ts # Property editing panel
|
||||
│ ├── wcc-record-button.ts # Recording button
|
||||
│ ├── wcc-recording-panel.ts # Recording options/preview
|
||||
│ └── wcctools.helpers.ts # Shared utilities
|
||||
├── services/
|
||||
│ └── recorder.service.ts # MediaRecorder abstraction
|
||||
└── pages/
|
||||
└── index.ts # Built-in pages
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- 🎨 Interactive component preview
|
||||
- 🔧 Real-time property editing with type detection
|
||||
- 🌓 Theme switching (light/dark)
|
||||
- 📱 Responsive viewport testing
|
||||
- 🎬 Screen recording with trimming
|
||||
- 🔗 URL-based deep linking
|
||||
391
ts_web/services/recorder.service.ts
Normal file
391
ts_web/services/recorder.service.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic
|
||||
*/
|
||||
|
||||
export interface IRecorderEvents {
|
||||
onDurationUpdate?: (duration: number) => void;
|
||||
onRecordingComplete?: (blob: Blob) => void;
|
||||
onAudioLevelUpdate?: (level: number) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onStreamEnded?: () => void;
|
||||
}
|
||||
|
||||
export interface IRecordingOptions {
|
||||
mode: 'viewport' | 'screen';
|
||||
audioDeviceId?: string;
|
||||
viewportElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export class RecorderService {
|
||||
// Recording state
|
||||
private mediaRecorder: MediaRecorder | null = null;
|
||||
private recordedChunks: Blob[] = [];
|
||||
private durationInterval: number | null = null;
|
||||
private _duration: number = 0;
|
||||
private _recordedBlob: Blob | null = null;
|
||||
private _isRecording: boolean = false;
|
||||
|
||||
// Audio monitoring state
|
||||
private audioContext: AudioContext | null = null;
|
||||
private audioAnalyser: AnalyserNode | null = null;
|
||||
private audioMonitoringInterval: number | null = null;
|
||||
private monitoringStream: MediaStream | null = null;
|
||||
|
||||
// Current recording stream
|
||||
private currentStream: MediaStream | null = null;
|
||||
|
||||
// Event callbacks
|
||||
private events: IRecorderEvents = {};
|
||||
|
||||
constructor(events?: IRecorderEvents) {
|
||||
if (events) {
|
||||
this.events = events;
|
||||
}
|
||||
}
|
||||
|
||||
// Public getters
|
||||
get isRecording(): boolean {
|
||||
return this._isRecording;
|
||||
}
|
||||
|
||||
get duration(): number {
|
||||
return this._duration;
|
||||
}
|
||||
|
||||
get recordedBlob(): Blob | null {
|
||||
return this._recordedBlob;
|
||||
}
|
||||
|
||||
// Update event callbacks
|
||||
setEvents(events: IRecorderEvents): void {
|
||||
this.events = { ...this.events, ...events };
|
||||
}
|
||||
|
||||
// ==================== Microphone Management ====================
|
||||
|
||||
async loadMicrophones(requestPermission: boolean = false): Promise<MediaDeviceInfo[]> {
|
||||
try {
|
||||
if (requestPermission) {
|
||||
// Request permission by getting a temporary stream
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter(d => d.kind === 'audioinput');
|
||||
} catch (error) {
|
||||
console.error('Error loading microphones:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async startAudioMonitoring(deviceId: string): Promise<void> {
|
||||
this.stopAudioMonitoring();
|
||||
|
||||
if (!deviceId) return;
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { deviceId: { exact: deviceId } }
|
||||
});
|
||||
|
||||
this.monitoringStream = stream;
|
||||
this.audioContext = new AudioContext();
|
||||
const source = this.audioContext.createMediaStreamSource(stream);
|
||||
this.audioAnalyser = this.audioContext.createAnalyser();
|
||||
this.audioAnalyser.fftSize = 256;
|
||||
source.connect(this.audioAnalyser);
|
||||
|
||||
const dataArray = new Uint8Array(this.audioAnalyser.frequencyBinCount);
|
||||
|
||||
this.audioMonitoringInterval = window.setInterval(() => {
|
||||
if (this.audioAnalyser) {
|
||||
this.audioAnalyser.getByteFrequencyData(dataArray);
|
||||
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
||||
const level = Math.min(100, (average / 128) * 100);
|
||||
this.events.onAudioLevelUpdate?.(level);
|
||||
}
|
||||
}, 50);
|
||||
} catch (error) {
|
||||
console.error('Error starting audio monitoring:', error);
|
||||
this.events.onAudioLevelUpdate?.(0);
|
||||
}
|
||||
}
|
||||
|
||||
stopAudioMonitoring(): void {
|
||||
if (this.audioMonitoringInterval) {
|
||||
clearInterval(this.audioMonitoringInterval);
|
||||
this.audioMonitoringInterval = null;
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
if (this.monitoringStream) {
|
||||
this.monitoringStream.getTracks().forEach(track => track.stop());
|
||||
this.monitoringStream = null;
|
||||
}
|
||||
this.audioAnalyser = null;
|
||||
}
|
||||
|
||||
// ==================== Recording Control ====================
|
||||
|
||||
async startRecording(options: IRecordingOptions): Promise<void> {
|
||||
try {
|
||||
// Stop audio monitoring before recording
|
||||
this.stopAudioMonitoring();
|
||||
|
||||
// Get video stream based on mode
|
||||
const displayMediaOptions: DisplayMediaStreamOptions = {
|
||||
video: {
|
||||
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
|
||||
} as MediaTrackConstraints,
|
||||
audio: false
|
||||
};
|
||||
|
||||
// Add preferCurrentTab hint for viewport mode
|
||||
if (options.mode === 'viewport') {
|
||||
(displayMediaOptions as any).preferCurrentTab = true;
|
||||
}
|
||||
|
||||
const videoStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
||||
|
||||
// If viewport mode, try to crop to viewport element using Element Capture API
|
||||
if (options.mode === 'viewport' && options.viewportElement) {
|
||||
try {
|
||||
if ('CropTarget' in window) {
|
||||
const cropTarget = await (window as any).CropTarget.fromElement(options.viewportElement);
|
||||
const [videoTrack] = videoStream.getVideoTracks();
|
||||
await (videoTrack as any).cropTo(cropTarget);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Element Capture not supported, recording full tab:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine video with audio if enabled
|
||||
let combinedStream = videoStream;
|
||||
if (options.audioDeviceId) {
|
||||
try {
|
||||
const audioStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { deviceId: { exact: options.audioDeviceId } }
|
||||
});
|
||||
combinedStream = new MediaStream([
|
||||
...videoStream.getVideoTracks(),
|
||||
...audioStream.getAudioTracks()
|
||||
]);
|
||||
} catch (audioError) {
|
||||
console.warn('Could not add audio:', audioError);
|
||||
}
|
||||
}
|
||||
|
||||
// Store stream for cleanup
|
||||
this.currentStream = combinedStream;
|
||||
|
||||
// Create MediaRecorder
|
||||
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
||||
? 'video/webm;codecs=vp9'
|
||||
: 'video/webm';
|
||||
|
||||
this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
|
||||
this.recordedChunks = [];
|
||||
|
||||
this.mediaRecorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
this.recordedChunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaRecorder.onstop = () => this.handleRecordingComplete();
|
||||
|
||||
// Handle stream ending (user clicks "Stop sharing")
|
||||
videoStream.getVideoTracks()[0].onended = () => {
|
||||
if (this._isRecording) {
|
||||
this.stopRecording();
|
||||
this.events.onStreamEnded?.();
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaRecorder.start(1000); // Capture in 1-second chunks
|
||||
|
||||
// Start duration timer
|
||||
this._duration = 0;
|
||||
this.durationInterval = window.setInterval(() => {
|
||||
this._duration++;
|
||||
this.events.onDurationUpdate?.(this._duration);
|
||||
}, 1000);
|
||||
|
||||
this._isRecording = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting recording:', error);
|
||||
this._isRecording = false;
|
||||
this.events.onError?.(error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
stopRecording(): void {
|
||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
||||
this.mediaRecorder.stop();
|
||||
}
|
||||
|
||||
if (this.durationInterval) {
|
||||
clearInterval(this.durationInterval);
|
||||
this.durationInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRecordingComplete(): void {
|
||||
// Create blob from recorded chunks
|
||||
this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
||||
|
||||
// Stop all tracks
|
||||
if (this.currentStream) {
|
||||
this.currentStream.getTracks().forEach(track => track.stop());
|
||||
this.currentStream = null;
|
||||
}
|
||||
|
||||
this._isRecording = false;
|
||||
this.events.onRecordingComplete?.(this._recordedBlob);
|
||||
}
|
||||
|
||||
// ==================== Trim & Export ====================
|
||||
|
||||
async exportTrimmedVideo(
|
||||
videoElement: HTMLVideoElement,
|
||||
trimStart: number,
|
||||
trimEnd: number
|
||||
): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Create a canvas for capturing frames
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = videoElement.videoWidth || 1280;
|
||||
canvas.height = videoElement.videoHeight || 720;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
reject(new Error('Could not get canvas context'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create canvas stream for video
|
||||
const canvasStream = canvas.captureStream(30);
|
||||
|
||||
// Try to capture audio from video element
|
||||
let combinedStream: MediaStream;
|
||||
|
||||
try {
|
||||
// Create audio context to capture video's audio
|
||||
const audioCtx = new AudioContext();
|
||||
const source = audioCtx.createMediaElementSource(videoElement);
|
||||
const destination = audioCtx.createMediaStreamDestination();
|
||||
source.connect(destination);
|
||||
source.connect(audioCtx.destination); // Also play through speakers
|
||||
|
||||
// Combine video (from canvas) and audio (from video element)
|
||||
combinedStream = new MediaStream([
|
||||
...canvasStream.getVideoTracks(),
|
||||
...destination.stream.getAudioTracks()
|
||||
]);
|
||||
|
||||
// Store audioCtx for cleanup
|
||||
const cleanup = () => {
|
||||
audioCtx.close();
|
||||
};
|
||||
|
||||
this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, cleanup, resolve, reject);
|
||||
} catch (audioError) {
|
||||
console.warn('Could not capture audio, recording video only:', audioError);
|
||||
combinedStream = canvasStream;
|
||||
this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, () => {}, resolve, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private recordTrimmedStream(
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
stream: MediaStream,
|
||||
trimStart: number,
|
||||
trimEnd: number,
|
||||
cleanup: () => void,
|
||||
resolve: (blob: Blob) => void,
|
||||
reject: (error: Error) => void
|
||||
): void {
|
||||
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
||||
? 'video/webm;codecs=vp9'
|
||||
: 'video/webm';
|
||||
|
||||
const recorder = new MediaRecorder(stream, { mimeType });
|
||||
const chunks: Blob[] = [];
|
||||
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
chunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = () => {
|
||||
cleanup();
|
||||
resolve(new Blob(chunks, { type: 'video/webm' }));
|
||||
};
|
||||
|
||||
recorder.onerror = (e) => {
|
||||
cleanup();
|
||||
reject(new Error('Recording error: ' + e));
|
||||
};
|
||||
|
||||
// Seek to trim start
|
||||
video.currentTime = trimStart;
|
||||
|
||||
video.onseeked = () => {
|
||||
// Start recording
|
||||
recorder.start(100);
|
||||
|
||||
// Start playing
|
||||
video.play();
|
||||
|
||||
// Draw frames to canvas
|
||||
const drawFrame = () => {
|
||||
if (video.currentTime >= trimEnd || video.paused || video.ended) {
|
||||
video.pause();
|
||||
video.onseeked = null;
|
||||
|
||||
// Give a small delay before stopping to ensure last frame is captured
|
||||
setTimeout(() => {
|
||||
if (recorder.state === 'recording') {
|
||||
recorder.stop();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
requestAnimationFrame(drawFrame);
|
||||
};
|
||||
|
||||
drawFrame();
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Cleanup ====================
|
||||
|
||||
reset(): void {
|
||||
this._recordedBlob = null;
|
||||
this.recordedChunks = [];
|
||||
this._duration = 0;
|
||||
this._isRecording = false;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stopRecording();
|
||||
this.stopAudioMonitoring();
|
||||
this.reset();
|
||||
|
||||
if (this.currentStream) {
|
||||
this.currentStream.getTracks().forEach(track => track.stop());
|
||||
this.currentStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es2017", "dom"],
|
||||
"declaration": true,
|
||||
"inlineSources": true,
|
||||
"inlineSourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "dist/",
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
3
ts_web/tspublish.json
Normal file
3
ts_web/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 1
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
|
||||
export {
|
||||
smartdelay
|
||||
};
|
||||
|
||||
import * as deesDomtools from '@designestate/dees-domtools';
|
||||
import * as deesDomtools from '@design.estate/dees-domtools';
|
||||
|
||||
export {
|
||||
deesDomtools
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2017",
|
||||
"moduleResolution": "Node"
|
||||
}
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
17
tslint.json
17
tslint.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
||||
Reference in New Issue
Block a user