diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..1f83b7d881 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Global ownership +# Bryan Ollendyke (btopro) is the primary maintainer and creator of HAXTheWeb +* @btopro + +# HAXTheWeb ecosystem is developed and maintained by: +# Bryan Ollendyke (@btopro) - Penn State University +# Copyright (c) 2015-2025 The Pennsylvania State University \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..451f1a9b13 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,72 @@ +## Related Issue + +Closes [ISSUE #XXXX](https://github.com/haxtheweb/issues/issues/XXXX) + +## Figma Link + + +## Description of Changes + + +### What changed: +- +- +- + +### Why this change was needed: + + +## Type of Change + +- [ ] 🐛 Bug fix (non-breaking change which fixes an issue) +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] 📚 Documentation update +- [ ] 🎨 Style/formatting changes +- [ ] ♻️ Code refactoring +- [ ] 🔧 Configuration changes + +## Testing Checklist + +- [ ] I have tested this change locally +- [ ] I have added/updated tests for my changes +- [ ] All existing tests pass +- [ ] I have tested on multiple browsers (if applicable) +- [ ] I have tested on mobile devices (if applicable) +- [ ] I have verified accessibility compliance +- [ ] I have tested with screen readers (if applicable) + +## Quality Assurance + +- [ ] I have followed the project's coding conventions +- [ ] I have updated documentation where necessary +- [ ] I have added comments to complex code +- [ ] My changes don't introduce console warnings/errors +- [ ] I have checked for performance implications + +## Ways to Test This Change + +1. +2. +3. + +## Screenshots/Recordings + + +### Before: + + +### After: + + +## Additional Notes + + +## Checklist + +- [ ] I have read the [contributing guidelines](../CONTRIBUTING.md) +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] Any dependent changes have been merged and published \ No newline at end of file diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000000..7dbaf7afa5 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,37 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://github.com/haxtheweb/webcomponents/blob/main/CLA.md' + # branch should not be protected + branch: 'main' + allowlist: 'btopro,*[bot]' + # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken + remote-organization-name: '' + remote-repository-name: '' + create-file-commit-message: 'Creating file for storing CLA Signatures' + signed-commit-message: 'CLA signed by $contributorName' + custom-notsigned-prcomment: 'Thank you for your contribution! Before we can merge your PR, we need you to sign our Contributor License Agreement (CLA). Please comment **"I have read the CLA Document and I hereby sign the CLA"** to agree to the [CLA terms](https://github.com/haxtheweb/webcomponents/blob/main/CLA.md).' + custom-pr-sign-comment: 'Thank you for signing the CLA! Your contribution will now be reviewed.' + custom-allsigned-prcomment: 'All contributors have signed the CLA. Thank you!' \ No newline at end of file diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml new file mode 100644 index 0000000000..bc6a53d987 --- /dev/null +++ b/.github/workflows/ossf_scorecard.yml @@ -0,0 +1,63 @@ +--- +# This workflow uses actions that are not certified by GitHub. They are provided by a third-party and are governed by separate terms of service, privacy policy, and support documentation. +name: OSSF Scorecard +on: + # For Branch-Protection check. Only the default branch is supported. See https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection. + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained. + schedule: + - cron: "0 0 * * 1" + push: + branches: [main, master] + workflow_dispatch: +# Declare default permissions as read only. +permissions: read-all +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-24.04 + # Delete the conditional below if you are using the OSSF Scorecard on a public repository. + if: ${{ github.event.repository.private == false }} + permissions: + # Needed if using Code Scanning alerts. + security-events: write + # Needed for GitHub OIDC token if publish_results is true. + id-token: write + # Uncomment the permissions below if you are using the OSSF Scorecard on a private repository. + # contents: read + # actions: read + # issues: read # To allow GraphQL ListCommits to work + # pull-requests: read # To allow GraphQL ListCommits to work + # checks: read # To detect SAST tools + steps: + - name: Check out the codebase + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) fine-grained personal access token. Uncomment the `repo_token` line below if you want to enable the Branch-Protection or Webhooks check on a *private* repository. + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Publish the results for public repositories to enable scorecard badges. For more details, see https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF format to the repository Actions tab. + - name: Upload artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: Upload SARIF results to code scanning + uses: github/codeql-action/upload-sarif@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/.gitignore b/.gitignore index 735c5df661..fb74bf6380 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ yarn-error.log yarn.lock test/index.html component-gallery.html +elements/video-player/demo/samples/ad/ elements/haxcms-elements/demo/files/ elements/grade-book/demo/psu/ elements/elmsln-apps/lib/lrnapp-studio-instructor/demo/* diff --git a/.wcflibcache.json b/.wcflibcache.json index cda3090190..db4df544ea 100644 --- a/.wcflibcache.json +++ b/.wcflibcache.json @@ -20,9 +20,6 @@ "devDependencies": { "web-animations-js": "2.3.2", "@haxtheweb/deduping-fix": "^9.0.0-alpha.0", - "@polymer/iron-demo-helpers": "3.1.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@webcomponents/webcomponentsjs": "^2.8.0", "gulp-babel": "8.0.0", "@web/dev-server": "0.4.6", "concurrently": "5.3.0", @@ -54,13 +51,10 @@ "lighthouse": "gulp lighthouse --gulpfile=gulpfile.cjs" }, "dependencies": { - "lit": "^3.3.0" + "lit": "^3.3.1" }, "devDependencies": { "@haxtheweb/deduping-fix": "^9.0.0-alpha.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@polymer/iron-demo-helpers": "3.1.0", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "5.3.0", "gulp-babel": "8.0.0", "polymer-build": "3.1.4", diff --git a/CLA-README.md b/CLA-README.md new file mode 100644 index 0000000000..e913b11f8f --- /dev/null +++ b/CLA-README.md @@ -0,0 +1,47 @@ +# Contributor License Agreement (CLA) Process + +## How it Works + +This repository uses an automated CLA (Contributor License Agreement) process to ensure legal compliance for all contributions. Here's how it works: + +### For New Contributors + +1. **Open a Pull Request**: When you submit your first pull request, the CLA bot will automatically post a comment asking you to sign the CLA. + +2. **Sign the CLA**: To sign the CLA, simply comment on your pull request with: + ``` + I have read the CLA Document and I hereby sign the CLA + ``` + +3. **Automatic Processing**: Once you sign, the bot will: + - Record your signature in the repository + - Update the pull request status + - Allow your PR to proceed with the review process + +### For Returning Contributors + +If you've already signed the CLA for this repository, you don't need to sign it again for future pull requests. The bot will automatically recognize you as a signed contributor. + +### Important Notes + +- **Exact Comment Required**: You must use the exact comment text: `I have read the CLA Document and I hereby sign the CLA` +- **Read the CLA**: Please read the [full CLA document](./CLA.md) before signing +- **One-Time Process**: You only need to sign once per repository +- **Bot Users**: Automated bot users are automatically allowlisted and don't need to sign + +### Troubleshooting + +If you're having issues with the CLA process: + +1. **Re-check Status**: Comment `recheck` on your pull request to have the bot re-evaluate your CLA status +2. **Contact Maintainers**: If problems persist, mention a maintainer in your pull request + +### Technical Details + +- CLA signatures are stored in `signatures/version1/cla.json` in this repository +- The CLA bot is implemented using the [contributor-assistant/github-action](https://github.com/contributor-assistant/github-action) +- No third-party services are used; everything is handled within GitHub + +--- + +**Questions?** Feel free to open an issue or ask in your pull request if you need help with the CLA process. \ No newline at end of file diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000000..99e652421d --- /dev/null +++ b/CLA.md @@ -0,0 +1,40 @@ +# Contributor License Agreement + +## HAXtheWeb Webcomponents Project + +Thank you for your interest in contributing to the HAXtheWeb Webcomponents project owned by Penn State University ("We" or "Us"). + +This Contributor License Agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please sign it and send it to Us. + +### 1. Definitions + +**"You"** (or **"Your"**) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Us. + +**"Contribution"** shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Us for inclusion in, or documentation of, any of the products owned or managed by Us (the "Work"). + +### 2. Grant of Rights + +#### Copyright License +Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Work, and to permit persons to whom the Work is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Work. + +#### Patent License +Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work. + +### 3. Representations + +You represent that: + +1. You are legally entitled to grant the above license. +2. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you have received permission to make Contributions on behalf of that employer, or your employer has waived such rights for your Contributions to Us. +3. Each of Your Contributions is Your original creation. +4. Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. + +### 4. You are not expected to provide support + +You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +--- + +**By commenting "I have read the CLA Document and I hereby sign the CLA" on a Pull Request, you are agreeing to the terms of this Contributor License Agreement.** \ No newline at end of file diff --git a/README.md b/README.md index 9032c4d1c0..afabd4132f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ +[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/haxtheweb/webcomponents/badge)](https://securityscorecards.dev/viewer/?uri=github.com/haxtheweb/webcomponents) +[![Community Support](https://badgen.net/badge/support/community/cyan?icon=awesome)](/SUPPORT.md) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](/CODE_OF_CONDUCT.md) +[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/EKYJAjqGhf) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) [![Lit](https://img.shields.io/badge/-Lit-324fff?style=flat&logo=data:image/svg%2bxml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTYwIDIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTYwIDgwdjgwbC00MC00MHptLTQwIDQwdjgwbDQwLTQwem0wLTgwdjgwbC00MC00MHptLTQwIDQwdjgwbDQwLTQwem0tNDAtNDB2ODBsNDAtNDB6bTQwLTQwdjgwbC00MC00MHptLTQwIDEyMHY4MGwtNDAtNDB6bS00MC00MHY4MGw0MC00MHoiLz48L3N2Zz4%3D)](https://lit.dev/) [![#HAXTheWeb](https://img.shields.io/badge/-HAXTheWeb-999999FF?style=flat&logo=data:image/svg%2bxml;base64,PHN2ZyBpZD0iZmVhMTExZTAtMjEwZC00Y2QwLWJhMWQtZGZmOTQyODc0Njg1IiBkYXRhLW5hbWU9IkxheWVyIDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE4NC40IDEzNS45NyI+PGRlZnM+PHN0eWxlPi5lMWJjMjAyNS0xODAwLTRkYzItODc4NS1jNDZlZDEwM2Y0OTJ7ZmlsbDojMjMxZjIwO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iZTFiYzIwMjUtMTgwMC00ZGMyLTg3ODUtYzQ2ZWQxMDNmNDkyIiBkPSJNNzguMDcsODMuNDVWNTVIODYuMnY4LjEzaDE2LjI2djQuMDdoNC4wN1Y4My40NUg5OC40VjY3LjE5SDg2LjJWODMuNDVaIi8+PHBvbHlnb24gcG9pbnRzPSIxNTMuMTMgNjMuNyAxNTMuMTMgNTEuMzkgMTQwLjU0IDUxLjM5IDE0MC41NCAzOS4wOSAxMjcuOTUgMzkuMDkgMTI3Ljk1IDI2Ljc5IDEwMi43OCAyNi43OSAxMDIuNzggMzkuMDkgMTE1LjM2IDM5LjA5IDExNS4zNiA1MS4zOSAxMjcuOTUgNTEuMzkgMTI3Ljk1IDYzLjcgMTQwLjU0IDYzLjcgMTQwLjU0IDc2IDEyNy4zNiA3NiAxMjcuMzYgODguMyAxMTQuNzggODguMyAxMTQuNzggMTAwLjYxIDEwMi4xOSAxMDAuNjEgMTAyLjE5IDExMi45MSAxMjcuMzYgMTEyLjkxIDEyNy4zNiAxMDAuNjEgMTM5Ljk1IDEwMC42MSAxMzkuOTUgODguMyAxNTIuNTQgODguMyAxNTIuNTQgNzYgMTY1LjcyIDc2IDE2NS43MiA2My43IDE1My4xMyA2My43Ii8+PHBvbHlnb24gcG9pbnRzPSIzMy4xMyA2My43IDMzLjEzIDUxLjM5IDQ1LjcyIDUxLjM5IDQ1LjcyIDM5LjA5IDU4LjMxIDM5LjA5IDU4LjMxIDI2Ljc5IDgzLjQ4IDI2Ljc5IDgzLjQ4IDM5LjA5IDcwLjg5IDM5LjA5IDcwLjg5IDUxLjM5IDU4LjMxIDUxLjM5IDU4LjMxIDYzLjcgNDUuNzIgNjMuNyA0NS43MiA3NiA1OC44OSA3NiA1OC44OSA4OC4zIDcxLjQ4IDg4LjMgNzEuNDggMTAwLjYxIDg0LjA3IDEwMC42MSA4NC4wNyAxMTIuOTEgNTguODkgMTEyLjkxIDU4Ljg5IDEwMC42MSA0Ni4zMSAxMDAuNjEgNDYuMzEgODguMyAzMy43MiA4OC4zIDMzLjcyIDc2IDIwLjU0IDc2IDIwLjU0IDYzLjcgMzMuMTMgNjMuNyIvPjwvc3ZnPg==)](https://haxtheweb.org/) diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..579b856956 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,31 @@ +# Support + +## Getting Help + +If you need help with this project, please use the following resources: + +### Community Support + +- **Discord**: Join our community Discord server for real-time help and discussion: [HAXTheWeb Discord](https://discord.gg/EKYJAjqGhf) +- **Documentation**: Visit our comprehensive documentation: [HAXTheWeb Docs](https://haxtheweb.org/documentation) +- **Issues**: For bug reports and feature requests, please use our unified issue queue: [HAXTheWeb Issues](https://github.com/haxtheweb/issues/issues) + +### Getting Started + +- Check out our [documentation](https://haxtheweb.org/documentation) for guides and tutorials +- Explore and play with HAX components: [HAX Magic Script Playground](https://hax.cloud/magicscript.html) +- Join the discussion on [Discord](https://discord.gg/EKYJAjqGhf) to connect with other developers + +### Before Opening an Issue + +Before creating a new issue, please: + +1. Search existing issues in our [unified issue queue](https://github.com/haxtheweb/issues/issues) +2. Check our [documentation](https://haxtheweb.org/documentation) +3. Ask for help on [Discord](https://discord.gg/EKYJAjqGhf) + +This helps keep our issue queue focused on actual bugs and feature requests. + +## Contributing + +We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get involved. \ No newline at end of file diff --git a/api/package.json b/api/package.json index 2f31b1cad0..70e34aace9 100644 --- a/api/package.json +++ b/api/package.json @@ -6,9 +6,9 @@ }, "dependencies": { "patch-package": "^8.0.0", - "lit-html": "3.3.0", + "lit-html": "3.3.1", "@lit-labs/ssr": "^3.3.1", - "lit": "3.3.0", + "lit": "3.3.1", "node-html-parser": "^6.1.13", "@haxtheweb/course-design": "^11.0.5", "@haxtheweb/lrn-math": "^11.0.5", diff --git a/automate-theme-screenshots.js b/automate-theme-screenshots.js new file mode 100644 index 0000000000..d14beb534c --- /dev/null +++ b/automate-theme-screenshots.js @@ -0,0 +1,142 @@ +#!/usr/bin/env node + +/** + * Complete HAX Theme Screenshot Automation Workflow + * + * This script provides the complete automation workflow for generating + * screenshots of all HAX themes and updating the registry. + * + * Usage: This is the automation guide for MCP puppeteer tools + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const THEMES_JSON_PATH = join(__dirname, 'elements/haxcms-elements/lib/themes.json'); +const SCREENSHOTS_DIR = join(dirname(THEMES_JSON_PATH), 'theme-screenshots'); + +// Load themes +const themes = JSON.parse(readFileSync(THEMES_JSON_PATH, 'utf8')); +const themeList = Object.entries(themes); + +console.log('\n=== HAX Theme Screenshot Automation Workflow ===\n'); + +/** + * STEP 1: Verify Setup + */ +console.log('✓ Themes loaded:', themeList.length); +console.log('✓ Screenshots directory:', SCREENSHOTS_DIR); +console.log('✓ Updated themes.json with paths'); + +/** + * STEP 2: Automation Commands + * These are the MCP puppeteer commands to run: + */ + +console.log('\n=== PUPPETEER AUTOMATION COMMANDS ===\n'); + +console.log('1. Navigate to demo site:'); +console.log(` puppeteer_navigate({"allowDangerous":true, "launchOptions":{"args":["--no-sandbox", "--disable-setuid-sandbox"], "headless":true}, "url":"http://localhost:8080"})`); + +console.log('\n2. Wait for HAXCMS to load (run this first):'); +console.log(` puppeteer_evaluate({"script":"new Promise(resolve => { const wait = () => { if (globalThis.HAXCMS && globalThis.HAXCMS.setTheme) { console.log('HAXCMS ready!'); const currentTheme = globalThis.HAXCMS.instance && globalThis.HAXCMS.instance.store && globalThis.HAXCMS.instance.store.manifest && globalThis.HAXCMS.instance.store.manifest.metadata && globalThis.HAXCMS.instance.store.manifest.metadata.theme && globalThis.HAXCMS.instance.store.manifest.metadata.theme.element; resolve({ready: true, currentTheme: currentTheme}); } else { console.log('Still waiting...'); setTimeout(wait, 2000); } }; wait(); })"})`); + +console.log('\n3. Theme switching and screenshot commands:'); +console.log(' For each theme, run these 3 commands in sequence:\n'); + +// Generate commands for first 5 themes as examples +const exampleThemes = themeList.slice(0, 5); +exampleThemes.forEach(([themeKey, themeData], index) => { + console.log(`--- Theme ${index + 1}: ${themeData.name} (${themeData.element}) ---`); + console.log(`a) Switch theme:`); + console.log(` puppeteer_evaluate({"script":"globalThis.HAXCMS.setTheme('${themeData.element}'); 'Theme switched to ${themeData.element}'"})`); + console.log(`b) Wait for theme to load (3 seconds):`); + console.log(` puppeteer_evaluate({"script":"new Promise(resolve => setTimeout(() => resolve('Theme loaded'), 3000))"})`); + console.log(`c) Take screenshot:`); + console.log(` puppeteer_screenshot({"height":800, "width":1280, "name":"${themeData.element}"})`); + console.log(''); +}); + +console.log(`... repeat for all ${themeList.length} themes\n`); + +/** + * STEP 3: Popular themes for quick demo + */ +const popularThemes = [ + 'polaris-flex-sidebar', + 'polaris-flex-theme', + 'clean-one', + 'journey-theme', + 'learn-two-theme', + 'spacebook-theme', + 'bootstrap-theme' +].filter(themeName => themes[themeName]); + +console.log('=== QUICK DEMO: Popular Themes ===\n'); +console.log(`Popular themes to screenshot first (${popularThemes.length} themes):\n`); + +popularThemes.forEach((themeKey, index) => { + const themeData = themes[themeKey]; + console.log(`${index + 1}. ${themeData.name}`); + console.log(` Element: ${themeData.element}`); + console.log(` Switch: globalThis.HAXCMS.setTheme('${themeData.element}')`); + console.log(` Screenshot: ${themeData.element}.jpg`); + console.log(''); +}); + +/** + * STEP 4: File Saving + */ +console.log('\n=== FILE SAVING INSTRUCTIONS ===\n'); +console.log('When saving screenshot files:'); +console.log('1. Save each screenshot as: {theme-element-name}.jpg'); +console.log(`2. Save to directory: ${SCREENSHOTS_DIR}`); +console.log('3. The themes.json is already updated with correct paths'); +console.log('4. Screenshots will be relative to themes.json location'); + +console.log('\n=== VERIFICATION ===\n'); +console.log('After screenshots are taken, verify:'); +console.log('1. Check screenshot files exist in theme-screenshots/'); +console.log('2. Themes.json has correct paths'); +console.log('3. V2 app-hax can load themes.json and display screenshots'); + +/** + * Export automation data for programmatic use + */ +export const automationWorkflow = { + totalThemes: themeList.length, + popularThemes: popularThemes, + screenshotsDir: SCREENSHOTS_DIR, + + // Generate command for specific theme + generateCommands: (themeElement) => { + const theme = Object.values(themes).find(t => t.element === themeElement); + if (!theme) return null; + + return { + switchTheme: `globalThis.HAXCMS.setTheme('${theme.element}')`, + wait: `new Promise(resolve => setTimeout(() => resolve('Theme loaded'), 3000))`, + screenshot: { + name: theme.element, + width: 1280, + height: 800 + } + }; + }, + + // All themes list + allThemes: themeList.map(([key, data]) => ({ + key, + element: data.element, + name: data.name, + screenshotPath: `theme-screenshots/${data.element}.jpg` + })) +}; + +console.log('\n=== READY FOR AUTOMATION ==='); +console.log('Run the puppeteer commands above to generate all theme screenshots!'); +console.log('The complete theme registry will be ready for v2 app-hax use-cases.'); \ No newline at end of file diff --git a/component-gallery.html b/component-gallery.html index 96bf45a840..31c8e6739c 100644 --- a/component-gallery.html +++ b/component-gallery.html @@ -370,12 +370,12 @@ + `; + } +} +customElements.define(AppHaxSiteDetails.tag, AppHaxSiteDetails); diff --git a/elements/app-hax/lib/v2/app-hax-site-login.js b/elements/app-hax/lib/v2/app-hax-site-login.js new file mode 100644 index 0000000000..b91defb6b8 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-site-login.js @@ -0,0 +1,370 @@ +import { LitElement, html, css } from "lit"; +import "@haxtheweb/simple-icon/lib/simple-icons.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-lite.js"; +import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; +import "@haxtheweb/rpg-character/rpg-character.js"; +import { store } from "./AppHaxStore.js"; +export class AppHaxSiteLogin extends DDDSuper(LitElement) { + // a convention I enjoy so you can change the tag name in 1 place + static get tag() { + return "app-hax-site-login"; + } + + // HTMLElement life-cycle, built in; use this for setting defaults + constructor() { + super(); + this.windowControllers = new AbortController(); + this.username = ""; + this.password = ""; + this.errorMSG = "Enter User name"; + this.hidePassword = true; + this.hasPass = false; + } + + // properties that you wish to use as data in HTML, CSS, and the updated life-cycle + static get properties() { + return { + ...super.properties, + username: { type: String }, + password: { type: String }, + errorMSG: { type: String }, + hidePassword: { type: Boolean }, + hasPass: { type: Boolean }, + }; + } + + firstUpdated() { + super.firstUpdated(); + setTimeout(() => { + this.shadowRoot.querySelector("input").focus(); + }, 0); + } + + // updated fires every time a property defined above changes + // this allows you to react to variables changing and use javascript to perform logic + // updated(changedProperties) { + // changedProperties.forEach((oldValue, propName) => { + // }); + // } + + // CSS - specific to Lit + static get styles() { + return [ + super.styles, + css` + :host { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: var(--ddd-spacing-6, 24px); + text-align: center; + font-family: var(--ddd-font-primary, sans-serif); + background: var(--ddd-theme-default-white, white); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + } + + rpg-character { + display: block; + margin: 0 0 var(--ddd-spacing-4, 16px) 0; + width: 120px; + height: 120px; + } + + #errorText { + color: var(--ddd-theme-default-original87Pink, #e4007c); + font-size: var(--ddd-font-size-xs, 14px); + margin: var(--ddd-spacing-2, 8px) 0; + min-height: var(--ddd-spacing-5, 20px); + font-weight: var(--ddd-font-weight-medium, 500); + } + + #inputcontainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--ddd-spacing-4, 16px); + } + + .form-group { + width: 100%; + position: relative; + } + + input { + width: 100%; + padding: var(--ddd-spacing-3, 12px); + border: var(--ddd-border-sm, 2px solid) + var(--ddd-theme-default-slateGray, #666); + border-radius: var(--ddd-radius-sm, 4px); + font-size: var(--ddd-font-size-s, 16px); + font-family: var(--ddd-font-primary, sans-serif); + box-sizing: border-box; + transition: border-color 0.2s ease; + background-color: var(--ddd-theme-default-limestoneMaxLight, #f5f5f5); + color: var(--ddd-theme-default-coalyGray, #444); + } + + input:focus { + outline: none; + border-color: var(--ddd-theme-default-nittanyNavy, #001e44); + } + + input::placeholder { + color: var(--ddd-theme-default-slateGray, #666); + text-transform: capitalize; + } + + button { + padding: var(--ddd-spacing-3, 12px) var(--ddd-spacing-4, 16px); + border-radius: var(--ddd-radius-sm, 4px); + font-size: var(--ddd-font-size-s, 16px); + font-weight: var(--ddd-font-weight-medium, 500); + font-family: var(--ddd-font-primary, sans-serif); + cursor: pointer; + transition: all 0.2s ease; + border: none; + display: flex; + align-items: center; + justify-content: center; + gap: var(--ddd-spacing-2, 8px); + width: 100%; + min-height: var(--ddd-spacing-10, 40px); + background: var(--ddd-theme-default-nittanyNavy, #001e44); + color: var(--ddd-theme-default-white, white); + } + + button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + button:hover:not(:disabled) { + background: var(--ddd-theme-default-keystoneYellow, #ffd100); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + transform: translateY(-1px); + box-shadow: var(--ddd-boxShadow-md); + } + + .notyou { + padding: var(--ddd-spacing-2, 8px); + font-size: var(--ddd-font-size-s, 16px); + color: var(--ddd-theme-default-coalyGray, #444); + } + + .notyou a { + color: var(--ddd-theme-default-nittanyNavy, #001e44); + text-decoration: underline; + cursor: pointer; + font-weight: var(--ddd-font-weight-medium, 500); + } + + .notyou a:hover { + color: var(--ddd-theme-default-keystoneYellow, #ffd100); + } + + .visibility-icon { + position: absolute; + right: var(--ddd-spacing-3, 12px); + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + color: var(--ddd-theme-default-slateGray, #666); + cursor: pointer; + padding: var(--ddd-spacing-1, 4px); + border-radius: var(--ddd-radius-xs, 2px); + transition: color 0.2s ease; + --simple-icon-width: var(--ddd-icon-xs, 16px); + --simple-icon-height: var(--ddd-icon-xs, 16px); + } + + .visibility-icon:hover { + color: var(--ddd-theme-default-nittanyNavy, #001e44); + } + + .external { + text-align: center; + width: 100%; + margin-top: var(--ddd-spacing-4, 16px); + } + + @media (max-width: 600px) { + :host { + padding: var(--ddd-spacing-4, 16px); + } + + rpg-character { + width: 80px; + height: 80px; + } + + button { + font-size: var(--ddd-font-size-xs, 14px); + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + } + } + `, + ]; + } + + // eslint-disable-next-line class-methods-use-this + checkUsername() { + // eslint-disable-next-line prefer-destructuring + const value = this.shadowRoot.querySelector("#username").value; + this.hidePassword = false; + this.errorMSG = ""; + this.username = value; + store.appEl.playSound("click2"); + setTimeout(() => { + this.shadowRoot.querySelector("input").focus(); + }, 0); + } + + // eslint-disable-next-line class-methods-use-this + async checkPassword() { + store.appEl.playSound("click2"); + // eslint-disable-next-line prefer-destructuring + const value = this.shadowRoot.querySelector("#password").value; + // attempt login and wait for response from the jwt-login tag via + // jwt-logged-in event @see _jwtLoggedIn + globalThis.dispatchEvent( + new CustomEvent("jwt-login-login", { + composed: true, + bubbles: true, + cancelable: false, + detail: { + username: this.username, + password: value, + }, + }), + ); + } + + // eslint-disable-next-line class-methods-use-this + reset() { + this.errorMSG = ""; + this.username = ""; + this.hasPass = false; + this.hidePassword = true; + } + + nameChange() { + this.username = this.shadowRoot.querySelector("#username").value; + } + + connectedCallback() { + super.connectedCallback(); + globalThis.addEventListener("jwt-logged-in", this._jwtLoggedIn.bind(this), { + signal: this.windowControllers.signal, + }); + globalThis.addEventListener( + "jwt-login-login-failed", + this._jwtLoginFailed.bind(this), + { signal: this.windowControllers.signal }, + ); + } + + disconnectedCallback() { + this.windowControllers.abort(); + super.disconnectedCallback(); + } + // implies that it failed to connect via the login credentials + _jwtLoginFailed(e) { + this.hidePassword = true; + this.errorMSG = "Invalid Username or Password"; + store.appEl.playSound("error"); + } + _jwtLoggedIn(e) { + if (e.detail) { + store.user = { + name: this.username, + }; + store.appEl.playSound("success"); + this.dispatchEvent( + new CustomEvent("simple-modal-hide", { + bubbles: true, + composed: true, + cancelable: true, + detail: {}, + }), + ); + store.toast(`Welcome ${this.username}! Let's go!`, 5000, { + hat: "construction", + }); + // just to be safe + store.appEl.reset(); + } + } + + passChange(e) { + const value = this.shadowRoot.querySelector("#password").value; + if (value) { + this.hasPass = true; + } else { + this.hasPass = false; + } + } + toggleViewPass(e) { + const password = this.shadowRoot.querySelector("#password"); + const type = + password.getAttribute("type") === "password" ? "text" : "password"; + password.setAttribute("type", type); + e.target.icon = type === "text" ? "lrn:visible" : "lrn:view-off"; + } + + render() { + return html` + +

${this.errorMSG}

+
+ ${this.hidePassword + ? html`
+ +
+ ` + : html`
+ Welcome back, ${this.username}! + Not you? +
+
+ + +
+ `} +
+ +
+
+ `; + } +} +customElements.define(AppHaxSiteLogin.tag, AppHaxSiteLogin); diff --git a/elements/app-hax/lib/v2/app-hax-steps.js b/elements/app-hax/lib/v2/app-hax-steps.js new file mode 100644 index 0000000000..35f805bef6 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-steps.js @@ -0,0 +1,1272 @@ +/* eslint-disable lit/attribute-value-entities */ +/* eslint-disable lit/binding-positions */ +/* eslint-disable import/no-unresolved */ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable class-methods-use-this */ +import { html, css, unsafeCSS } from "lit"; +import { SimpleColors } from "@haxtheweb/simple-colors/simple-colors.js"; +import { autorun, toJS } from "mobx"; +import { store } from "./AppHaxStore.js"; +import { localStorageSet } from "@haxtheweb/utils/utils.js"; +import "scrollable-component/index.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-lite.js"; +import { MicroFrontendRegistry } from "@haxtheweb/micro-frontend-registry/micro-frontend-registry.js"; +import { enableServices } from "@haxtheweb/micro-frontend-registry/lib/microServices.js"; +import "./app-hax-site-button.js"; +import "./app-hax-hat-progress.js"; +import "./app-hax-button.js"; + +const homeIcon = new URL("../assets/images/Home.svg", import.meta.url).href; +const disabledCircle = new URL( + "../assets/images/DisabledCircle.svg", + import.meta.url, +).href; +const transparentCircle = new URL( + "../assets/images/TransparentCircle.svg", + import.meta.url, +).href; +const enabledCircle = new URL( + "../assets/images/EnabledCircle.svg", + import.meta.url, +).href; + +const themeContext = { + collection: ["collections-theme", "bootstrap-theme"], + blog: ["haxor-slevin"], + course: ["clean-one", "clean-two", "learn-two-theme"], + website: ["polaris-flex-theme"], + training: ["training-theme"], + import: ["clean-one", "clean-two", "learn-two-theme"], +}; +export class AppHaxSteps extends SimpleColors { + static get tag() { + return "app-hax-steps"; + } + + constructor() { + super(); + this.unlockComingSoon = false; + this.unlockTerrible = false; + this.windowControllers = new AbortController(); + this.nameTyped = ""; + this.stepRoutes = []; + this._progressReady = false; + this.step = null; + this.loaded = false; + this.themeNames = []; + this.appSettings = {}; + autorun(() => { + this.appSettings = toJS(store.appSettings); + const contextKey = toJS(store.site.structure); + this.themeNames = Object.keys(this.appSettings.themes).filter( + (value) => + contextKey && + themeContext[contextKey] && + themeContext[contextKey].includes(value), + ); + }); + autorun(() => { + this.dark = toJS(store.darkMode); + }); + autorun(() => { + localStorageSet("app-hax-step", toJS(store.step)); + }); + autorun(() => { + localStorageSet("app-hax-site", toJS(store.site)); + this.step = store.stepTest(this.step); + }); + autorun(() => { + if (toJS(store.createSiteSteps) && toJS(store.location)) { + this.step = store.stepTest(this.step); + } + }); + // routes, but only the ones that have a step property + autorun(() => { + const routes = toJS(store.routes); + this.stepRoutes = routes.filter((item) => item.step); + }); + } + + static get properties() { + return { + ...super.properties, + step: { type: Number, reflect: true }, + stepRoutes: { type: Array }, + themeNames: { type: Array }, + unlockComingSoon: { + type: Boolean, + reflect: true, + attribute: "unlock-coming-soon", + }, + unlockTerrible: { + type: Boolean, + reflect: true, + attribute: "unlock-terrible", + }, + loaded: { type: Boolean, reflect: true }, + appSettings: { type: Object }, + nameTyped: { type: String }, + }; + } + + // step 1 + chooseStructure(e) { + if (!e.target.comingSoon) { + const { value } = e.target; + store.site.structure = value; + // @note for now, auto select type and theme if making a course + // we might want to revisit this in the future + if (value === "course") { + store.site.type = "own"; + store.site.theme = "clean-one"; + } + if (value === "blog") { + store.site.type = "own"; + store.site.theme = "haxor-slevin"; + } + if (value === "collection") { + store.site.type = "own"; + store.site.theme = "collections-theme"; + } + if (value === "website") { + store.site.type = "own"; + store.site.theme = "polaris-flex-theme"; + } + if (value === "training") { + store.site.type = "own"; + store.site.theme = "training-theme"; + } + store.appEl.playSound("click2"); + } + } + + // step 2 + chooseType(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + store.site.type = type; + store.appEl.playSound("click2"); + } + } + // step 2, doc import + async docxImport(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + import( + "@haxtheweb/file-system-broker/lib/docx-file-system-broker.js" + ).then(async (e) => { + // enable core services + enableServices(["haxcms"]); + // get the broker for docx selection + const broker = globalThis.FileSystemBroker.requestAvailability(); + const file = await broker.loadFile("docx"); + // tee up as a form for upload + const formData = new FormData(); + formData.append("method", "site"); // this is a site based importer + formData.append("type", toJS(store.site.structure)); + formData.append("upload", file); + this.setProcessingVisual(); + const response = await MicroFrontendRegistry.call( + "@haxcms/docxToSite", + formData, + ); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + if (response.data.files) { + store.itemFiles = response.data.files; + } + // invoke a file broker for a docx file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(".docx", "") + .replace("outline", "") + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`File did not return valid HTML structure`); + } + }); + } + } + // evolution import + async evoImport(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + import("@haxtheweb/file-system-broker/file-system-broker.js").then( + async (e) => { + // enable core services + enableServices(["haxcms"]); + // get the broker for docx selection + const broker = globalThis.FileSystemBroker.requestAvailability(); + const file = await broker.loadFile("zip"); + // tee up as a form for upload + const formData = new FormData(); + formData.append("method", "site"); // this is a site based importer + formData.append("type", toJS(store.site.structure)); + formData.append("upload", file); + // local end point + stupid JWT thing + this.setProcessingVisual(); + const response = await MicroFrontendRegistry.call( + "@haxcms/evolutionToSite", + formData, + null, + null, + "?jwt=" + toJS(store.AppHaxAPI.jwt), + ); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + // invoke a file broker for a docx file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(".zip", "") + .replace("outline", "") + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`File did not return valid HTML structure`); + } + }, + ); + } + } + // gitbook import endpoint + async gbImport(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + let gbURL = globalThis.prompt("URL for the Gitbook repo"); + enableServices(["haxcms"]); + this.setProcessingVisual(); + const response = await MicroFrontendRegistry.call( + "@haxcms/gitbookToSite", + { md: gbURL }, + ); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + if (response.data.files) { + store.itemFiles = response.data.files; + } + // invoke a file broker for a docx file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`Repo did not return valid structure`); + } + } + } + async importFromURL(e) { + const { type, prompt, callback, param } = e.target; + if (!e.target.comingSoon) { + let promptUrl = globalThis.prompt(prompt); + enableServices(["haxcms"]); + this.setProcessingVisual(); + const params = {}; + params[param] = promptUrl; + const response = await MicroFrontendRegistry.call(callback, params); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + if (response.data.files) { + store.itemFiles = response.data.files; + } + // invoke a file broker for a docx file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`Repo did not return valid structure`); + } + } + } + // notion import endpoint + async notionImport(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + let notionUrl = globalThis.prompt("URL for the Github Notion repo"); + enableServices(["haxcms"]); + this.setProcessingVisual(); + const response = await MicroFrontendRegistry.call( + "@haxcms/notionToSite", + { repoUrl: notionUrl }, + ); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + if (response.data.files) { + store.itemFiles = response.data.files; + } + // invoke a file broker for a docx file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`Repo did not return valid structure`); + } + } + } + // pressbooks import endpoint + async pressbooksImport(e) { + if (!e.target.comingSoon) { + const { type } = e.target; + import( + "@haxtheweb/file-system-broker/lib/docx-file-system-broker.js" + ).then(async (e) => { + // enable core services + enableServices(["haxcms"]); + // get the broker for docx selection + const broker = globalThis.FileSystemBroker.requestAvailability(); + const file = await broker.loadFile("html"); + // tee up as a form for upload + const formData = new FormData(); + formData.append("method", "site"); // this is a site based importer + formData.append("type", toJS(store.site.structure)); + formData.append("upload", file); + this.setProcessingVisual(); + const response = await MicroFrontendRegistry.call( + "@haxcms/pressbooksToSite", + formData, + ); + store.toast(`Processed!`, 300); + // must be a valid response and have at least SOME html to bother attempting + if ( + response.status == 200 && + response.data && + response.data.contents != "" + ) { + store.items = response.data.items; + if (response.data.files) { + store.itemFiles = response.data.files; + } + // invoke a file broker for a html file + // send to the endpoint and wait + // if it comes back with content, then we engineer details off of it + this.nameTyped = response.data.filename + .replace(".html", "") + .replace("outline", "") + .replace(/\s/g, "") + .replace(/-/g, "") + .toLowerCase(); + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").value = this.nameTyped; + this.shadowRoot.querySelector("#sitename").select(); + }, 800); + store.site.type = type; + store.site.theme = "clean-one"; + store.appEl.playSound("click2"); + } else { + store.appEl.playSound("error"); + store.toast(`File did not return valid HTML structure`); + } + }); + } + } + // makes guy have hat on, shows it's doing something + setProcessingVisual() { + let loadingIcon = globalThis.document.createElement("simple-icon-lite"); + loadingIcon.icon = "hax:loading"; + loadingIcon.style.setProperty("--simple-icon-height", "40px"); + loadingIcon.style.setProperty("--simple-icon-width", "40px"); + loadingIcon.style.height = "150px"; + loadingIcon.style.marginLeft = "8px"; + store.toast(`Processing`, 60000, { + hat: "construction", + slot: loadingIcon, + }); + } + // step 3 + chooseTheme(e) { + if (!e.target.comingSoon) { + const { value } = e.target; + store.site.theme = value; + store.appEl.playSound("click2"); + } + } + + // step 4 + chooseName() { + if (this.nameTyped !== "") { + const value = this.shadowRoot.querySelector("#sitename").value; + store.site.name = value; + store.appEl.playSound("click2"); + } + } + + progressReady(e) { + if (e.detail) { + this._progressReady = true; + if (this.step === 5) { + setTimeout(() => { + this.shadowRoot.querySelector("app-hax-hat-progress").process(); + }, 300); + } + } + } + + updated(changedProperties) { + if (super.updated) { + super.updated(changedProperties); + } + changedProperties.forEach((oldValue, propName) => { + // set input field to whats in store if we have it + if (this.step === 4 && propName === "step" && this.shadowRoot) { + this.shadowRoot.querySelector("#sitename").value = toJS( + store.site.name, + ); + } + // progress + if ( + this.step === 5 && + propName === "step" && + this.shadowRoot && + this._progressReady + ) { + setTimeout(() => { + this.shadowRoot.querySelector("app-hax-hat-progress").process(); + }, 600); + } + // update the store for step when it changes internal to our step flow + if (propName === "step") { + store.step = this.step; + } + if (propName === "unlockTerrible" && this[propName]) { + Object.keys(themeContext).forEach((key) => { + themeContext[key] = [ + ...themeContext[key], + "terrible-themes", + "terrible-productionz-themes", + "terrible-outlet-themes", + "terrible-best-themes", + "terrible-resume-themes", + ]; + }); + const contextKey = toJS(store.site.structure); + this.themeNames = Object.keys(this.appSettings.themes).filter( + (value) => + contextKey && + themeContext[contextKey] && + themeContext[contextKey].includes(value), + ); + } + }); + } + + connectedCallback() { + super.connectedCallback(); + globalThis.addEventListener("resize", this.maintainScroll.bind(this), { + signal: this.windowControllers.signal, + }); + globalThis.addEventListener("popstate", this.popstateListener.bind(this), { + signal: this.windowControllers.signal, + }); + } + + disconnectedCallback() { + this.windowControllers.abort(); + super.disconnectedCallback(); + } + + // see if user navigates forward or backward while in app + popstateListener(e) { + // filter out vaadin link clicks which have a state signature + if (e.type === "popstate" && e.state === null) { + // a lot going on here, just to be safe + try { + // the delay allows clicking for step to change, process, and then testing it + setTimeout(() => { + const link = e.target.document.location.pathname.split("/").pop(); + // other links we don't care about validating state + if (link.includes("createSite")) { + const step = parseInt(link.replace("createSite-step-", "")); + if (step < store.stepTest(step)) { + this.shadowRoot.querySelector("#link-step-" + step).click(); + } else if (step > store.stepTest(step)) { + store.toast(`Please select an option`); + this.step = store.stepTest(step); + // forces state by maintaining where we are + this.shadowRoot.querySelector("#link-step-" + this.step).click(); + } + } + }, 0); + } catch (e) {} + } + } + + // account for resizing + maintainScroll() { + if (this.shadowRoot && this.step) { + this.scrollToThing(`#step-${this.step}`, { + behavior: "instant", + block: "start", + inline: "nearest", + }); + // account for an animated window drag... stupid. + setTimeout(() => { + this.scrollToThing(`#step-${this.step}`, { + behavior: "instant", + block: "start", + inline: "nearest", + }); + }, 100); + } + } + + firstUpdated(changedProperties) { + if (super.firstUpdated) { + super.firstUpdated(changedProperties); + } + setTimeout(() => { + // ensure paint issues not a factor for null step + if (this.step === null) { + this.step = 1; + } + this.scrollToThing(`#step-${this.step}`, { + behavior: "instant", + block: "start", + inline: "nearest", + }); + }, 100); + + autorun(() => { + // verify we are in the site creation process + if (toJS(store.createSiteSteps) && toJS(store.appReady)) { + const location = toJS(store.location); + if (location.route && location.route.step && location.route.name) { + // account for an animated window drag... stupid. + setTimeout(() => { + this.scrollToThing("#".concat(location.route.name), { + behavior: "smooth", + block: "start", + inline: "nearest", + }); + /// just for step 4 since it has an input + if (location.route.step === 4 && store.stepTest(4) === 4) { + setTimeout(() => { + this.shadowRoot.querySelector("#sitename").focus(); + this.scrollToThing(`#step-4`, { + behavior: "instant", + block: "start", + inline: "nearest", + }); + }, 800); + } + }, 300); // this delay helps w/ initial paint timing but also user perception + // there's a desire to have a delay especialy when tapping things of + // about 300ms + } + } + }); + autorun(() => { + if ( + this.shadowRoot && + toJS(store.createSiteSteps) && + toJS(store.appReady) + ) { + const activeItem = toJS(store.activeItem); + if ( + activeItem && + activeItem.name && + activeItem.step && + !this.__overrideProgression + ) { + this.shadowRoot + .querySelector("#link-".concat(activeItem.name)) + .click(); + } + } + }); + } + + /** + * Yet another reason Apple doesn't let us have nice things. + * This detects the NONSTANDARD BS VERSION OF SCROLLINTOVIEW + * and then ensures that it incorrectly calls to scroll into view + * WITHOUT the wonderful params that ALL OTHER BROWSERS ACCEPT + * AND MAKE OUR LIVES SO WONDERFUL TO SCROLL TO THINGS SMOOTHLY + */ + scrollToThing(sel, props) { + const isSafari = globalThis.safari !== undefined; + if ( + this.shadowRoot.querySelector(".carousel-with-snapping-item.active-step") + ) { + this.shadowRoot + .querySelector(".carousel-with-snapping-item.active-step") + .classList.remove("active-step"); + } + if (isSafari) { + this.shadowRoot.querySelector(sel).scrollIntoView(); + } else { + this.shadowRoot.querySelector(sel).scrollIntoView(props); + } + this.shadowRoot.querySelector(sel).classList.add("active-step"); + } + + static get styles() { + return [ + super.styles, + css` + :host { + display: block; + } + scrollable-component { + --scrollbar-width: 0px; + --scrollbar-height: 0px; + --scrollbar-padding: 0; + --viewport-overflow-x: hidden; + overflow: hidden; + } + #grid-container { + display: grid; + grid-template-columns: 160px 160px 160px; + background: transparent; + } + .carousel-with-snapping-track { + display: grid; + grid-auto-flow: column; + grid-gap: 30px; + } + .carousel-with-snapping-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: normal; + scroll-snap-align: center; + scroll-snap-stop: always; + scrollbar-gutter: stable; + width: var(--viewport-width); + font-size: 1.5rem; + text-align: center; + overflow-x: hidden; + max-height: 60vh; + padding-top: 1vh; + } + #step-links { + padding: 0; + margin: 0; + } + ul, + li { + list-style: none; + } + li { + vertical-align: middle; + display: inline-flex; + margin: 5px; + } + li.step { + border-radius: 50%; + background-color: transparent; + } + li a { + font-size: 12px; + color: var(--simple-colors-default-theme-grey-12, white); + text-decoration: none; + padding: 5px; + width: 20px; + height: 20px; + line-height: 20px; + margin: 0; + display: block; + border: 0; + border-radius: 50%; + background-repeat: no-repeat; + background-size: 30px 30px; + background-color: var(--simple-colors-default-theme-grey-1, white); + background-image: url("${unsafeCSS(enabledCircle)}"); + transition: + 0.3s ease-in-out background, + 0.3s ease-in-out color; + transition-delay: 0.6s, 0.3s; + } + li a[disabled] { + background-image: url("${unsafeCSS(disabledCircle)}"); + pointer-events: none; + color: var(--simple-colors-default-theme-grey-7, grey); + user-select: none; + } + li[disabled] { + background-color: grey; + } + li.active-step a { + background-color: orange; + background-image: url("${unsafeCSS(transparentCircle)}"); + } + app-hax-button { + padding: 10px 0px 10px 0px; + background: transparent; + } + #theme-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + img { + pointer-events: none; + } + #themeContainer { + width: 70vw; + height: 55vh; + } + .theme-button { + background-color: transparent; + color: var(--simple-colors-default-theme-grey-12, white); + border: none; + margin: 8px; + padding: 8px; + width: 245px; + } + + .theme-button div { + font-size: 14px; + margin-top: 12px; + } + .theme-button:focus, + .theme-button:hover { + outline: 4px solid var(--app-hax-accent-color, var(--accent-color)); + outline-offset: 4px; + background-color: transparent; + border: none; + cursor: pointer; + } + #sitename { + font-size: 32px; + padding: 8px; + width: 40vw; + } + #homebtn { + --simple-icon-height: 30px; + --simple-icon-width: 30px; + border-radius: 50%; + cursor: pointer; + background-color: var(--simple-colors-default-theme-grey-1, white); + } + .homelnk { + background-image: none; + display: flex; + padding: 0; + margin: 0; + height: 30px; + width: 30px; + } + app-hax-site-button { + justify-content: center; + --app-hax-site-button-width: 35vw; + --app-hax-site-button-min-width: 240px; + } + app-hax-hat-progress { + height: 400px; + width: 400px; + display: block; + } + + @media (max-width: 800px) { + .theme-button { + width: unset; + padding: 0; + } + .theme-button div { + font-size: 12px; + margin-top: 8px; + } + .theme-button img { + height: 70px; + } + app-hax-site-button { + width: 320px; + max-width: 60vw; + --app-hax-site-button-font-size: 2.5vw; + } + #sitename { + width: 70vw; + font-size: 20px; + } + #grid-container { + grid-template-columns: 120px 120px 120px; + } + } + @media (max-height: 600px) { + .carousel-with-snapping-item { + padding-top: 4px; + max-height: 57vh; + } + #sitename { + width: 40vw; + font-size: 14px; + } + app-hax-hat-progress { + transform: scale(0.5); + margin-top: -18vh; + } + } + @media (max-width: 500px) { + app-hax-hat-progress { + transform: scale(0.5); + margin-top: -15vh; + } + } + @media (max-height: 400px) { + .carousel-with-snapping-item { + padding-top: 4px; + max-height: 40vh; + } + app-hax-hat-progress { + transform: scale(0.3); + } + .carousel-with-snapping-item.active-step app-hax-hat-progress { + position: fixed; + top: 20%; + left: 20%; + } + } + `, + ]; + } + + progressFinished(e) { + if (e.detail) { + this.loaded = true; + store.appEl.playSound("success"); + // focus the button for going to the site + e.target.shadowRoot.querySelector(".game").focus(); + this.scrollToThing(`#step-${this.step}`, { + behavior: "instant", + block: "start", + inline: "nearest", + }); + } + } + + typeKey() { + this.nameTyped = this.shadowRoot.querySelector("#sitename").value; + } + keydown(e) { + // some trapping for common characters that make us sad + if ( + [ + " ", + "/", + "\\", + "&", + "#", + "?", + "+", + "=", + "{", + "}", + "|", + "^", + "~", + "[", + "]", + "`", + '"', + "'", + ].includes(e.key) + ) { + store.appEl.playSound("error"); + store.toast(`"${e.key}" is not allowed. Use - or _`); + e.preventDefault(); + } else if (e.key === "Enter") { + this.chooseName(); + } else if ( + ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"].includes(e.key) + ) { + // do nothing, directional keys for modifying word + } else { + store.appEl.playSound("click"); + } + } + + stepLinkClick(e) { + const clickedStep = parseInt(e.target.getAttribute("data-step"), 10); + if (this.step < clickedStep) { + e.preventDefault(); + } else if (e.target.getAttribute("data-step") === null) { + store.createSiteSteps = false; + store.appMode = "home"; + this.nameTyped = ""; + store.siteReady = false; + store.site.structure = null; + store.site.type = null; + store.site.theme = null; + store.site.name = null; + } + // means user went backwards + else if (this.step > clickedStep) { + this.nameTyped = ""; + store.siteReady = false; + if (clickedStep === 1) { + store.site.structure = null; + store.site.type = null; + store.site.theme = null; + store.site.name = null; + } else if (clickedStep === 2) { + store.site.type = null; + store.site.theme = null; + store.site.name = null; + } else if (clickedStep === 3) { + store.site.theme = null; + store.site.name = null; + } else if (clickedStep === 4) { + store.site.name = null; + } + this.step = clickedStep; + } + } + + renderTypes(step) { + const structure = toJS(store.site.structure); + var template = html``; + switch (structure) { + case "collection": + template = html` + + + `; + break; + default: + case "course": + template = html` + `; + break; + case "website": + template = html` `; + break; + case "training": + template = html` `; + break; + case "blog": + template = html` `; + break; + case "import": + template = html` + + + + + + + `; + break; + } + return template; + } + + render() { + return html` +
+ + + + +
+ `; + } +} +customElements.define(AppHaxSteps.tag, AppHaxSteps); diff --git a/elements/app-hax/lib/v2/app-hax-toast.js b/elements/app-hax/lib/v2/app-hax-toast.js new file mode 100644 index 0000000000..0b389800c4 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-toast.js @@ -0,0 +1,60 @@ +import { autorun, toJS } from "mobx"; +import { store } from "./AppHaxStore.js"; +import { RPGCharacterToast } from "@haxtheweb/haxcms-elements/lib/core/ui/rpg-character-toast/rpg-character-toast.js"; + +export class AppHaxToast extends RPGCharacterToast { + static get tag() { + return "app-hax-toast"; + } + + constructor() { + super(); + this.windowControllers = new AbortController(); + autorun(() => { + this.userName = toJS(store.user.name); + }); + autorun(() => { + this.darkMode = toJS(store.darkMode); + }); + } + + connectedCallback() { + super.connectedCallback(); + globalThis.addEventListener( + "haxcms-toast-hide", + this.hideSimpleToast.bind(this), + { signal: this.windowControllers.signal }, + ); + + globalThis.addEventListener( + "haxcms-toast-show", + this.showSimpleToast.bind(this), + { signal: this.windowControllers.signal }, + ); + } + + hideSimpleToast(e) { + this.hide(); + } + + /** + * life cycle, element is removed from the DOM + */ + disconnectedCallback() { + this.windowControllers.abort(); + super.disconnectedCallback(); + } +} +customElements.define(AppHaxToast.tag, AppHaxToast); +globalThis.AppHaxToast = globalThis.AppHaxToast || {}; + +globalThis.AppHaxToast.requestAvailability = () => { + if (!globalThis.AppHaxToast.instance) { + globalThis.AppHaxToast.instance = globalThis.document.createElement( + AppHaxToast.tag, + ); + globalThis.document.body.appendChild(globalThis.AppHaxToast.instance); + } + return globalThis.AppHaxToast.instance; +}; +export const AppHaxToastInstance = globalThis.AppHaxToast.requestAvailability(); diff --git a/elements/app-hax/lib/v2/app-hax-top-bar.js b/elements/app-hax/lib/v2/app-hax-top-bar.js new file mode 100644 index 0000000000..33616ccfe5 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-top-bar.js @@ -0,0 +1,130 @@ +// dependencies / things imported +import { LitElement, html, css } from "lit"; +import "./app-hax-wired-toggle.js"; + +// top bar of the UI +export class AppHaxTopBar extends LitElement { + // a convention I enjoy so you can change the tag name in 1 place + static get tag() { + return "app-hax-top-bar"; + } + + constructor() { + super(); + this.editMode = false; + } + + static get properties() { + return { + editMode: { + type: Boolean, + reflect: true, + attribute: "edit-mode", + }, + }; + } + + static get styles() { + return css` + :host { + --bg-color: var(--app-hax-background-color); + --accent-color: var(--app-hax-accent-color); + --top-bar-height: 46px; + display: block; + height: var(--top-bar-height); + } + + /* @media (prefers-color-scheme: dark) { + :root { + --accent-color: white; + color: var(--accent-color); + + } + + :host { + background-color: black; + } + } */ + + .topBar { + overflow: hidden; + background-color: var(--bg-color); + color: var(--accent-color); + height: var(--top-bar-height); + text-align: center; + vertical-align: middle; + border-bottom: 2px solid var(--app-hax-accent-color); + display: grid; + grid-template-columns: 32.5% 35% 32.5%; + transition: border-bottom 0.6s ease-in-out; + } + + /* .topBar > div { + background-color: rgba(255, 255, 255, 0.8); + border: 1px solid black; + } */ + + .topBar .left { + text-align: left; + height: var(--top-bar-height); + vertical-align: text-top; + } + + .topBar .center { + text-align: center; + height: var(--top-bar-height); + vertical-align: text-top; + } + + .topBar .right { + text-align: right; + height: var(--top-bar-height); + vertical-align: text-top; + } + @media (max-width: 640px) { + .topBar .left { + opacity: 0; + pointer-events: none; + } + .topBar .center { + text-align: left; + } + .topBar .right { + text-align: left; + } + #home { + display: none; + } + app-hax-search-bar { + display: none; + } + .topBar { + grid-template-columns: 0% 35% 65%; + display: inline-grid; + } + } + `; + } + + render() { + return html` + + `; + } +} +customElements.define(AppHaxTopBar.tag, AppHaxTopBar); diff --git a/elements/app-hax/lib/v2/app-hax-use-case-filter.js b/elements/app-hax/lib/v2/app-hax-use-case-filter.js new file mode 100644 index 0000000000..77e3c690ec --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-use-case-filter.js @@ -0,0 +1,1192 @@ +/* eslint-disable no-return-assign */ +import { LitElement, html, css } from "lit"; +import "@haxtheweb/simple-tooltip/simple-tooltip.js"; +import "@haxtheweb/simple-icon/lib/simple-icons.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-lite.js"; +import { store } from "./AppHaxStore.js"; +import "./app-hax-use-case.js"; +import "./app-hax-search-results.js"; +import "./app-hax-filter-tag.js"; +import "./app-hax-scroll-button.js"; +import "./app-hax-site-creation-modal.js"; + +export class AppHaxUseCaseFilter extends LitElement { + static get tag() { + return "app-hax-use-case-filter"; + } + + constructor() { + super(); + this.windowControllers = new AbortController(); + this.searchTerm = ""; + this.disabled = false; + this.showSearch = false; + this.items = []; + this.filteredItems = []; + this.filteredSites = []; + this.activeFilters = []; + this.filters = []; + this.searchQuery = ""; + this.demoLink = ""; + this.errorMessage = ""; + this.loading = false; + this.selectedCardIndex = null; + this.returningSites = []; + this.allFilters = new Set(); + this.dark = false; + this.isLoggedIn = false; + + // Listen to store changes for dark mode and manifest updates + if (typeof store !== "undefined") { + import("mobx").then(({ autorun, toJS }) => { + autorun(() => { + this.dark = toJS(store.darkMode); + }); + // Watch for manifest changes and update site results + autorun(() => { + const manifest = toJS(store.manifest); + if (manifest && manifest.items && manifest.items.length > 0) { + this.updateSiteResults(); + } + }); + }); + } + } + + static get properties() { + return { + searchTerm: { type: String }, + showSearch: { type: Boolean, reflect: true, attribute: "show-search" }, + showFilter: { type: Boolean, reflect: true, attribute: "show-filter" }, + disabled: { type: Boolean, reflect: true }, + items: { type: Array }, + filteredItems: { type: Array }, + filteredSites: { type: Array }, + activeFilters: { type: Array }, + filters: { type: Array }, + searchQuery: { type: String }, + demoLink: { type: String }, + errorMessage: { type: String }, + loading: { type: Boolean }, + selectedCardIndex: { type: Number }, + returningSites: { type: Array }, + allFilters: { attribute: false }, + dark: { type: Boolean, reflect: true }, + isLoggedIn: { type: Boolean }, + }; + } + + static get styles() { + return [ + css` + :host { + overflow: hidden; + display: block; + max-width: 100%; + font-family: var(--ddd-font-primary, sans-serif); + } + .contentSection { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: var(--ddd-spacing-12, 48px); + width: 100%; + margin: 0; + padding: 0; + box-sizing: border-box; + } + .leftSection, + .rightSection { + display: flex; + flex-direction: column; + flex: 1 1 0; + } + .leftSection { + width: 240px; + min-width: 200px; + max-width: 260px; + margin-left: 0; + margin-right: var(--ddd-spacing-1, 4px); + padding-top: 0; + box-sizing: border-box; + align-self: flex-start; + } + .rightSection { + flex: 1; + min-width: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: visible; + } + .template-results { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + width: 100%; + min-height: 330px; + box-sizing: border-box; + gap: var(--ddd-spacing-2, 16px); + } + #returnToSection { + width: 100%; + } + #returnToSection app-hax-search-results { + width: 100%; + min-height: 280px; + box-sizing: border-box; + height: 300px; + } + :host(:not([show-filter])) app-hax-search-results { + width: 100%; + } + + h2, + .returnTo h2, + .startNew h2 { + font-family: var(--ddd-font-primary, sans-serif); + font-size: var(--ddd-font-size-l, 24px); + color: var(--app-hax-accent-color, var(--accent-color)); + margin: 0 0 var(--ddd-spacing-4, 16px) 0; + } + .startNew, + .returnTo { + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + margin: 0; + } + .upper-filter { + margin-bottom: var(--ddd-spacing-4, 16px); + position: relative; + display: inline-block; + width: 100%; + } + input[type="text"] { + width: 100%; + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-2, 8px) + var(--ddd-spacing-2, 8px) var(--ddd-spacing-8, 32px); + font-size: var(--ddd-font-size-xs, 12px); + border-radius: var(--ddd-radius-sm, 4px); + border: var(--ddd-border-xs, 1px solid); + border-color: var(--ddd-theme-default-slateGray, #666); + background: var(--ddd-theme-default-white, white); + color: var(--ddd-theme-default-coalyGray, #222); + transition: all 0.2s ease; + box-sizing: border-box; + font-family: var(--ddd-font-primary, sans-serif); + margin: 0; + min-height: var(--ddd-spacing-8, 32px); + } + :host([dark]) input[type="text"], + body.dark-mode input[type="text"] { + background: var(--ddd-theme-default-coalyGray, #333); + color: var(--ddd-theme-default-white, white); + border-color: var(--ddd-theme-default-slateGray, #666); + } + input[type="text"]:focus { + border: var(--ddd-border-md, 2px solid); + border-color: var(--ddd-theme-default-keystoneYellow, #ffd100); + background: var(--ddd-theme-default-white, white); + outline: none; + } + :host([dark]) input[type="text"]:focus, + body.dark-mode input[type="text"]:focus { + background: var(--ddd-theme-default-coalyGray, #333); + border-color: var(--ddd-theme-default-keystoneYellow, #ffd100); + } + .search-icon { + position: absolute; + left: var(--ddd-spacing-2, 8px); + top: 50%; + transform: translateY(-50%); + font-size: var(--ddd-font-size-xs, 14px); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + pointer-events: none; + z-index: 1; + --simple-icon-width: var(--ddd-icon-3xs, 20px); + --simple-icon-height: var(--ddd-icon-3xs, 20px); + } + :host([dark]) .search-icon, + body.dark-mode .search-icon { + color: var(--ddd-theme-default-white, white); + } + .filter { + position: relative; + top: 0; + display: flex; + flex-direction: column; + gap: var(--ddd-spacing-5, 20px); + background: var(--ddd-theme-default-white, white); + border-radius: var(--ddd-radius-lg, 12px); + box-shadow: var(--ddd-boxShadow-lg); + border: var(--ddd-border-xs, 1px solid); + border-color: var(--ddd-theme-default-slateGray, #666); + padding: var(--ddd-spacing-6, 24px); + margin-top: 0; + margin-bottom: 0; + box-sizing: border-box; + font-family: var(--ddd-font-primary, sans-serif); + transition: box-shadow 0.2s ease; + } + :host([dark]) .filter, + body.dark-mode .filter { + background: var(--ddd-theme-default-coalyGray, #222); + border-color: var(--ddd-theme-default-slateGray, #666); + color: var(--ddd-theme-default-white, white); + } + .filter:hover { + box-shadow: var(--ddd-boxShadow-xl); + } + .filterButtons { + display: flex; + flex-direction: column; + gap: var(--ddd-spacing-3, 12px); + margin-top: 0; + border: none; + padding: 0; + margin: 0; + } + .filter-btn { + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--ddd-spacing-1, 4px); + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + border-radius: var(--ddd-radius-sm, 4px); + border: var(--ddd-border-xs, 1px solid) transparent; + background: var(--ddd-theme-default-limestoneGray, #f5f5f5); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + font-size: var(--ddd-font-size-3xs, 11px); + font-family: var(--ddd-font-primary, sans-serif); + font-weight: var(--ddd-font-weight-medium, 500); + cursor: pointer; + box-shadow: var(--ddd-boxShadow-sm); + transition: all 0.2s ease; + outline: none; + min-height: var(--ddd-spacing-7, 28px); + text-align: left; + } + :host([dark]) .filter-btn, + body.dark-mode .filter-btn { + background: var(--ddd-theme-default-slateGray, #444); + color: var(--ddd-theme-default-white, white); + } + .filter-btn.active, + .filter-btn:active { + background: var(--ddd-theme-default-nittanyNavy, #001e44); + color: var(--ddd-theme-default-white, white); + border-color: var(--ddd-theme-default-keystoneYellow, #ffd100); + box-shadow: var(--ddd-boxShadow-md); + } + :host([dark]) .filter-btn.active, + :host([dark]) .filter-btn:active, + body.dark-mode .filter-btn.active, + body.dark-mode .filter-btn:active { + background: var(--ddd-theme-default-keystoneYellow, #ffd100); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + border-color: var(--ddd-theme-default-white, white); + } + .filter-btn:hover, + .filter-btn:focus { + background: var(--ddd-theme-default-slateGray, #666); + color: var(--ddd-theme-default-white, white); + transform: translateY(-1px); + } + .filter-btn:focus { + outline: var(--ddd-border-md, 2px solid) + var(--ddd-theme-default-keystoneYellow, #ffd100); + outline-offset: var(--ddd-spacing-1, 2px); + } + :host([dark]) .filter-btn:hover, + :host([dark]) .filter-btn:focus, + body.dark-mode .filter-btn:hover, + body.dark-mode .filter-btn:focus { + background: var(--ddd-theme-default-limestoneGray, #f5f5f5); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + } + .filter-btn .icon { + font-size: var(--ddd-font-size-3xs, 12px); + color: inherit; + display: flex; + align-items: center; + flex-shrink: 0; + width: var(--ddd-icon-3xs, 20px); + height: var(--ddd-icon-3xs, 20px); + } + .filter-btn .icon simple-icon-lite { + color: inherit; + --simple-icon-width: var(--ddd-icon-3xs, 20px); + --simple-icon-height: var(--ddd-icon-3xs, 20px); + } + .filter-btn.active .icon, + .filter-btn.active .icon simple-icon-lite { + color: inherit; + } + :host([dark]) .filter-btn.active .icon, + :host([dark]) .filter-btn.active .icon simple-icon-lite, + body.dark-mode .filter-btn.active .icon, + body.dark-mode .filter-btn.active .icon simple-icon-lite { + color: inherit; + } + .filter-btn:hover .icon simple-icon-lite, + .filter-btn:focus .icon simple-icon-lite { + color: inherit; + } + input[type="checkbox"] { + display: none; + } + .reset-button { + margin-top: var(--ddd-spacing-1, 4px); + background: var(--ddd-theme-default-original87Pink, #e4007c); + border: var(--ddd-border-xs, 1px solid) transparent; + color: var(--ddd-theme-default-white, white); + border-radius: var(--ddd-radius-sm, 4px); + font-size: var(--ddd-font-size-3xs, 11px); + font-family: var(--ddd-font-primary, sans-serif); + font-weight: var(--ddd-font-weight-medium, 500); + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + display: flex; + align-items: center; + justify-content: center; + gap: var(--ddd-spacing-1, 4px); + box-shadow: var(--ddd-boxShadow-sm); + cursor: pointer; + transition: all 0.2s ease; + min-height: var(--ddd-spacing-7, 28px); + } + .reset-button:hover, + .reset-button:focus { + background: var(--ddd-theme-default-beaver70, #c85c2c); + transform: translateY(-1px); + } + .reset-button:focus { + outline: var(--ddd-border-md, 2px solid) + var(--ddd-theme-default-keystoneYellow, #ffd100); + outline-offset: var(--ddd-spacing-1, 2px); + } + :host([dark]) .reset-button, + body.dark-mode .reset-button { + background: var(--ddd-theme-default-beaver70, #c85c2c); + } + :host([dark]) .reset-button:hover, + :host([dark]) .reset-button:focus, + body.dark-mode .reset-button:hover, + body.dark-mode .reset-button:focus { + background: var(--ddd-theme-default-original87Pink, #e4007c); + } + .collapseFilter { + display: none; + } + + /* Visually hidden content for screen readers */ + .visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; + } + + @media (max-width: 780px) { + .contentSection { + display: block; + } + .leftSection { + width: 100%; + max-width: 100%; + margin-bottom: var(--ddd-spacing-4, 16px); + position: relative; + } + .rightSection { + width: 100%; + } + :host([show-filter]) .filter { + display: flex; + width: 250px; + max-width: 20vw; + } + :host .collapseFilter { + display: flex; + } + h4, + .returnTo h4, + .startNew h4 { + font-size: var(--ddd-font-size-m, 20px); + } + .template-results { + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--ddd-spacing-3, 12px); + } + } + + @media (max-width: 600px) { + .contentSection { + display: block; + margin: 0 var(--ddd-spacing-2, 8px); + padding-right: var(--ddd-spacing-2, 8px); + } + .leftSection { + width: 100%; + max-width: 100%; + margin-bottom: var(--ddd-spacing-3, 12px); + position: relative; + } + .rightSection { + width: 100%; + } + :host([show-filter]) .filter { + display: flex; + width: 200px; + max-width: 20vw; + } + :host .collapseFilter { + display: flex; + } + h4, + .returnTo h4, + .startNew h4 { + font-size: var(--ddd-font-size-s, 18px); + } + .template-results { + grid-template-columns: 1fr; + gap: var(--ddd-spacing-2, 8px); + } + } + + @media (max-width: 480px) { + .contentSection { + margin: 0 var(--ddd-spacing-1, 4px); + padding-right: var(--ddd-spacing-1, 4px); + } + h4, + .returnTo h4, + .startNew h4 { + font-size: var(--ddd-font-size-s, 16px); + margin: 0 0 var(--ddd-spacing-3, 12px) 0; + } + .template-results { + grid-template-columns: 1fr; + gap: var(--ddd-spacing-2, 8px); + } + #returnToSection app-hax-search-results { + min-width: 100%; + } + } + .no-results { + font-size: var(--ddd-font-size-s, 16px); + color: light-dark( + var(--ddd-theme-default-coalyGray, #222), + var(--ddd-theme-default-white, white) + ); + } + `, + ]; + } + + testKeydown(e) { + if (e.key === "Escape" || e.key === "Enter") { + this.toggleSearch(); + } + } + + toggleFilterVisibility() { + this.showFilter = !this.showFilter; + } + + handleFilterKeydown(e, filter) { + // Handle keyboard interaction for filter labels (Space and Enter) + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + this.toggleFilterByButton(filter); + } + } + + render() { + return html` +
+
+ +
+ +
+ +
+

Return to...

+
+ + +
+
+ + +
+

Create New Site

+
+
+ ${this.filteredItems.length} templates available +
+ ${this.filteredItems.length > 0 + ? this.filteredItems.map( + (item, index) => html` +
+ this.toggleDisplay(index, e)} + @continue-action=${() => this.continueAction(index)} + > +
+ `, + ) + : html`

+ No templates match the current filters. Try adjusting your + search or clearing filters. +

`} +
+
+
+
+ + + + `; + } + + iconForFilter(filter) { + switch (filter.toLowerCase()) { + case "blog": + return "lrn:write"; + case "brochure": + return "icons:description"; + case "course": + return "hax:lesson"; + case "portfolio": + return "icons:perm-identity"; + case "blank": + return "hax:bricks"; + default: + return "icons:label"; + } + } + toggleFilterByButton(filter) { + if (this.activeFilters.includes(filter)) { + this.activeFilters = this.activeFilters.filter((f) => f !== filter); + } else { + this.activeFilters = [...this.activeFilters, filter]; + } + this.applyFilters(); + this.requestUpdate(); + } + + connectedCallback() { + super.connectedCallback(); + globalThis.addEventListener("jwt-logged-in", this._jwtLoggedIn.bind(this), { + signal: this.windowControllers.signal, + }); + } + + disconnectedCallback() { + this.windowControllers.abort(); + super.disconnectedCallback(); + } + + _jwtLoggedIn(e) { + // When login status changes to true, refresh skeleton list + if (e.detail === true) { + this.isLoggedIn = true; + this.updateSkeletonResults(); + this.updateSiteResults(); + } else if (e.detail === false) { + this.isLoggedIn = false; + } + } + + firstUpdated() { + super.firstUpdated(); + // Only fetch if already logged in on first load + // Otherwise wait for jwt-logged-in event + if (this.isLoggedIn) { + this.updateSkeletonResults(); + this.updateSiteResults(); + } + } + + updated(changedProperties) { + if ( + changedProperties.has("searchQuery") || + changedProperties.has("activeFilters") || + changedProperties.has("items") + ) { + this.applyFilters(); + } + } + + toggleSearch() { + if (!this.disabled) { + this.shadowRoot.querySelector("#searchField").value = ""; + store.appEl.playSound("click"); + this.showSearch = !this.showSearch; + setTimeout(() => { + this.shadowRoot.querySelector("#searchField").focus(); + }, 300); + } + } + + toggleSelection(index) { + if (this.activeUseCase === index) { + this.activeUseCase = false; // Deselect if the same card is clicked + } else { + this.activeUseCase = index; // Select the new card + } + this.requestUpdate(); + } + + handleSearch(event) { + const searchTerm = event.target.value.toLowerCase(); + this.searchTerm = searchTerm; + store.searchTerm = searchTerm; // Update store with search term + + // Filter templates (skeletons and blank themes) + this.filteredItems = [ + ...this.items.filter( + (item) => + (item.dataType === "skeleton" || item.dataType === "blank") && + (item.useCaseTitle.toLowerCase().includes(searchTerm) || + (item.useCaseTag && + item.useCaseTag.some((tag) => + tag.toLowerCase().includes(searchTerm), + ))), + ), + ]; + + // Filter returning sites + this.filteredSites = [ + ...this.items.filter( + (item) => + item.dataType === "site" && + ((item.originalData.title && + item.originalData.title.toLowerCase().includes(searchTerm)) || + (item.useCaseTag && + item.useCaseTag.some((tag) => + tag.toLowerCase().includes(searchTerm), + ))), + ), + ]; + + this.requestUpdate(); + } + + toggleFilter(event) { + const filterValue = event.target.value; + + if (this.activeFilters.includes(filterValue)) { + this.activeFilters = [ + ...this.activeFilters.filter((f) => f !== filterValue), + ]; + } else { + this.activeFilters = [...this.activeFilters, filterValue]; + } + this.applyFilters(); + } + + applyFilters() { + const lowerCaseQuery = this.searchTerm.toLowerCase(); + + // Filter skeletons and blank themes (from this.items) + this.filteredItems = [ + ...this.items.filter((item) => { + if (item.dataType !== "skeleton" && item.dataType !== "blank") + return false; + const matchesSearch = + lowerCaseQuery === "" || + item.useCaseTitle.toLowerCase().includes(lowerCaseQuery) || + (item.useCaseTag && + item.useCaseTag.some((tag) => + tag.toLowerCase().includes(lowerCaseQuery), + )); + + const matchesFilters = + this.activeFilters.length === 0 || + (item.useCaseTag && + this.activeFilters.some((filter) => + item.useCaseTag.includes(filter), + )); + + return matchesSearch && matchesFilters; + }), + ]; + // Filter sites (from this.returningSites) + this.filteredSites = [ + ...this.returningSites.filter((item) => { + if (item.dataType !== "site") return false; + const siteCategory = + (item.originalData.metadata && + item.originalData.metadata.site && + item.originalData.metadata.site.category) || + []; + const matchesSearch = + lowerCaseQuery === "" || + (item.originalData.category && + item.originalData.category && + item.originalData.category.includes(lowerCaseQuery)) || + (item.useCaseTag && + item.useCaseTag.some((tag) => + tag.toLowerCase().includes(lowerCaseQuery), + )); + const matchesFilters = + this.activeFilters.length === 0 || + this.activeFilters.some((filter) => { + return siteCategory.includes(filter); + }); + return matchesSearch && matchesFilters; + }), + ]; + } + + removeFilter(event) { + const filterToRemove = event.detail; + this.activeFilters = this.activeFilters.filter((f) => f !== filterToRemove); + this.applyFilters(); // Re-filter results + this.requestUpdate(); + } + + resetFilters() { + this.searchTerm = ""; + store.searchTerm = ""; + this.activeFilters = []; + // Show all templates (skeletons and blank themes) and all sites + this.filteredItems = [ + ...this.items.filter( + (item) => item.dataType === "skeleton" || item.dataType === "blank", + ), + ]; + this.filteredSites = [...this.returningSites]; + + // Clear UI elements + this.shadowRoot.querySelector("#searchField").value = ""; + this.shadowRoot + .querySelectorAll('input[type="checkbox"]') + .forEach((cb) => (cb.checked = false)); + + this.requestUpdate(); + } + + updateSkeletonResults() { + this.loading = true; + this.errorMessage = ""; + + // Require configured endpoint + if (!store.appSettings || !store.appSettings.skeletonsList) { + this.errorMessage = "Skeletons endpoint not configured"; + this.loading = false; + return; + } + + // Build promises: backend call for skeletons, appSettings themes or fallback fetch + const skeletonsPromise = + store.AppHaxAPI && store.AppHaxAPI.makeCall + ? store.AppHaxAPI.makeCall("skeletonsList") + : Promise.reject(new Error("API not available")); + + // Prefer themes from appSettings (injected by backend); fallback to static file + let themesPromise; + if (store.appSettings && store.appSettings.themes) { + themesPromise = Promise.resolve(store.appSettings.themes); + } else { + const themesUrl = new URL( + "../../../haxcms-elements/lib/themes.json", + import.meta.url, + ).href; + themesPromise = fetch(themesUrl).then((response) => { + if (!response.ok) throw new Error(`Failed Themes (${response.status})`); + return response.json(); + }); + } + + Promise.allSettled([skeletonsPromise, themesPromise]) + .then(([skeletonsData, themesData]) => { + // Process skeletons data (expects { status, data: [] }) + const skeletonArray = + skeletonsData.value && + skeletonsData.value.data && + Array.isArray(skeletonsData.value.data) + ? skeletonsData.value.data + : []; + const skeletonItems = + skeletonArray.map((item) => { + let tags = []; + if (Array.isArray(item.category)) { + tags = item.category.filter( + (c) => typeof c === "string" && c.trim() !== "", + ); + } else if ( + typeof item.category === "string" && + item.category.trim() !== "" + ) { + tags = [item.category.trim()]; + } + if (tags.length === 0) tags = ["Empty"]; + tags.forEach((tag) => this.allFilters.add(tag)); // Add to global Set + + const icons = Array.isArray(item.attributes) + ? item.attributes.map((attr) => ({ + icon: attr.icon || "", + tooltip: attr.tooltip || "", + })) + : []; + let thumbnailPath = item.image || ""; + if (thumbnailPath && thumbnailPath.startsWith("@haxtheweb/")) { + // Navigate from current file to simulate node_modules structure and resolve path + // Current file: elements/app-hax/lib/v2/app-hax-use-case-filter.js + // Need to go up to webcomponents root, then navigate to the package + // In node_modules: @haxtheweb/package-name becomes ../../../@haxtheweb/package-name + const packagePath = "../../../../" + thumbnailPath; + thumbnailPath = new URL(packagePath, import.meta.url).href; + } + return { + dataType: "skeleton", + useCaseTitle: item.title || "Untitled Template", + useCaseImage: thumbnailPath || "", + useCaseDescription: item.description || "", + useCaseIcon: icons, + useCaseTag: tags, + demoLink: item["demo-url"] || "#", + skeletonUrl: item["skeleton-url"] || "", + originalData: item, + }; + }) || []; + + // Process themes data into blank use cases (filter out hidden themes) + const themeSource = themesData.value || {}; + const themeItems = Object.values(themeSource) + .filter((theme) => !theme.hidden) // Exclude hidden system/debug themes + .map((theme) => { + let tags = []; + if (Array.isArray(theme.category)) { + tags = theme.category.filter( + (c) => typeof c === "string" && c.trim() !== "", + ); + } else if ( + typeof theme.category === "string" && + theme.category.trim() !== "" + ) { + tags = [theme.category.trim()]; + } + if (tags.length === 0) tags = ["Blank"]; + tags.forEach((tag) => this.allFilters.add(tag)); // Add to global Set + + // Simple icon array for blank themes + const icons = [{ icon: "icons:build", tooltip: "Customizable" }]; + + // Resolve thumbnail path using import.meta.url navigation + let thumbnailPath = theme.thumbnail || ""; + if (thumbnailPath && thumbnailPath.startsWith("@haxtheweb/")) { + // Navigate from current file to simulate node_modules structure and resolve path + // Current file: elements/app-hax/lib/v2/app-hax-use-case-filter.js + // Need to go up to webcomponents root, then navigate to the package + // In node_modules: @haxtheweb/package-name becomes ../../../@haxtheweb/package-name + const packagePath = "../../../../" + thumbnailPath; + thumbnailPath = new URL(packagePath, import.meta.url).href; + } + + return { + dataType: "blank", + useCaseTitle: theme.name || "Untitled Theme", + useCaseImage: thumbnailPath || "", + useCaseDescription: + theme.description || "Start with a blank site using this theme", + useCaseIcon: icons, + useCaseTag: tags, + demoLink: "#", // Blank themes don't have demos + originalData: theme, + }; + }); + // Combine skeleton and theme items + this.items = [...skeletonItems, ...themeItems]; + this.filters = Array.from(this.allFilters).sort(); // Set AFTER all items + + if (this.items.length === 0 && !this.errorMessage) { + this.errorMessage = "No Templates Found"; + } + + this.resetFilters(); + }) + .catch((error) => { + this.errorMessage = `Failed to load data: ${error.message}`; + this.items = []; + this.filters = []; + }) + .finally(() => { + this.loading = false; + }); + } + + updateSiteResults() { + this.loading = true; + this.errorMessage = ""; + + try { + // Use store.manifest data instead of demo JSON + const sitesData = store.manifest; + + if (!sitesData || !sitesData.items) { + throw new Error("No manifest data available"); + } + + const siteItems = Array.isArray(sitesData.items) + ? sitesData.items.map((item) => { + let categorySource = + item.metadata && item.metadata.site + ? item.metadata.site.category + : null; + let tags = []; + if (Array.isArray(categorySource)) { + tags = categorySource.filter( + (c) => typeof c === "string" && c.trim() !== "", + ); + } else if ( + typeof categorySource === "string" && + categorySource.trim() !== "" + ) { + tags = [categorySource.trim()]; + } + if (tags.length === 0) tags = ["Empty"]; + tags.forEach((tag) => this.allFilters.add(tag)); // Add to global Set + return { + dataType: "site", + useCaseTag: tags, + originalData: item, + ...item, // this spreads every prop into this area that way it can be filtered correctly + }; + }) + : []; + this.returningSites = [...siteItems]; + this.filters = Array.from(this.allFilters).sort(); // Set AFTER all items + this.filteredSites = [...siteItems]; + + if (siteItems.length === 0 && !this.errorMessage) { + this.errorMessage = "No Sites Found"; + } + + this.requestUpdate(); + this.loading = false; + } catch (error) { + this.errorMessage = `Failed to load data: ${error.message}`; + this.returningSites = []; + this.filteredSites = []; + this.filters = []; + this.loading = false; + } + } + + toggleDisplay(index, event) { + const isSelected = event.detail.isSelected; + + if (this.selectedCardIndex !== null && this.selectedCardIndex !== index) { + // Deselect the previously selected card + this.filteredItems[this.selectedCardIndex].isSelected = false; + this.filteredItems[this.selectedCardIndex].showContinue = false; + } + + if (isSelected) { + // Select the new card + this.selectedCardIndex = index; + } else { + // Deselect the current card + this.selectedCardIndex = null; + } + + this.filteredItems[index].isSelected = isSelected; + this.filteredItems[index].showContinue = isSelected; + this.requestUpdate(); + } + + async continueAction(index) { + const selectedTemplate = this.filteredItems[index]; + const modal = this.shadowRoot.querySelector("#siteCreationModal"); + + if (modal && selectedTemplate) { + // Set the template details in the modal + modal.title = selectedTemplate.useCaseTitle; + modal.description = selectedTemplate.useCaseDescription; + modal.source = selectedTemplate.useCaseImage; + modal.template = selectedTemplate.useCaseTitle; + + // Handle skeleton templates by loading the skeleton file + if ( + selectedTemplate.dataType === "skeleton" && + selectedTemplate.skeletonUrl + ) { + try { + const skeletonUrl = new URL( + selectedTemplate.skeletonUrl, + import.meta.url, + ).href; + const response = await fetch(skeletonUrl); + if (response.ok) { + const skeletonData = await response.json(); + // Store skeleton data for use in site creation + modal.skeletonData = skeletonData; + modal.themeElement = + (skeletonData.site && skeletonData.site.theme) || "clean-one"; + } else { + console.warn(`Failed to load skeleton from ${skeletonUrl}`); + modal.themeElement = "clean-one"; // fallback + } + } catch (error) { + console.warn(`Error loading skeleton:`, error); + modal.themeElement = "clean-one"; // fallback + } + } else if ( + selectedTemplate.dataType === "blank" && + selectedTemplate.originalData.element + ) { + modal.themeElement = selectedTemplate.originalData.element; + } else { + modal.themeElement = "clean-one"; // fallback + } + + // Open the modal + modal.openModal(); + } + } + + handleModalClosed(event) { + // If modal was cancelled (not completed), reset selected states + if (event.detail && event.detail.cancelled) { + // Reset the selected card if one was selected + if ( + this.selectedCardIndex !== null && + this.filteredItems[this.selectedCardIndex] + ) { + this.filteredItems[this.selectedCardIndex].isSelected = false; + this.filteredItems[this.selectedCardIndex].showContinue = false; + this.selectedCardIndex = null; + this.requestUpdate(); + } + } + console.log("Site creation modal closed", event.detail); + } +} +customElements.define("app-hax-use-case-filter", AppHaxUseCaseFilter); diff --git a/elements/app-hax/lib/v2/app-hax-use-case.js b/elements/app-hax/lib/v2/app-hax-use-case.js new file mode 100644 index 0000000000..561244ebc9 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-use-case.js @@ -0,0 +1,405 @@ +/* eslint-disable no-return-assign */ +import { LitElement, html, css } from "lit"; +import "@haxtheweb/simple-tooltip/simple-tooltip.js"; +import { store } from "./AppHaxStore.js"; + +export class AppHaxUseCase extends LitElement { + static get tag() { + return "app-hax-use-case"; + } + + constructor() { + super(); + this.title = ""; + this.description = ""; + this.source = ""; + this.demoLink = ""; + this.iconImage = []; + this.isSelected = false; + this.showContinue = false; + } + + static get properties() { + return { + title: { type: String }, + description: { type: String }, + source: { type: String }, + demoLink: { type: String }, + iconImage: { type: Array }, + isSelected: { type: Boolean, reflect: true }, + showContinue: { type: Boolean }, + }; + } + + updated(changedProperties) {} + + static get styles() { + return [ + css` + :host { + display: flex; + flex-direction: column; + text-align: left; + margin: 4px; + font-family: var(--ddd-font-primary); + color: light-dark( + var(--ddd-theme-default-nittanyNavy), + var(--ddd-theme-default-white) + ); + background-color: light-dark( + white, + var(--ddd-theme-default-coalyGray, #222) + ); + border: var(--ddd-border-sm); + border-color: light-dark( + var(--ddd-theme-default-slateGray, #c4c4c4), + var(--ddd-theme-default-slateGray, #666) + ); + box-shadow: light-dark( + 0px 1px 3px rgba(0, 0, 0, 0.1), + 0px 1px 3px rgba(0, 0, 0, 0.2) + ); + border-radius: var(--ddd-radius-sm, 4px); + cursor: pointer; + transition: all 0.2s ease; + } + :host(:hover), + :host(:focus-within) { + transform: translateY(-2px) scale(1.02); + border-color: light-dark( + var(--ddd-theme-default-keystoneYellow, #ffd100), + var(--ddd-theme-default-keystoneYellow, #ffd100) + ); + box-shadow: light-dark( + 4px 8px 24px rgba(28, 28, 28, 0.15), + 4px 8px 24px rgba(0, 0, 0, 0.5) + ); + } + .cardContent { + padding: 8px 12px 16px; + } + .image img { + border-top-right-radius: 6px; + border-top-left-radius: 6px; + border-bottom: solid var(--ddd-theme-default-nittanyNavy) 8px; + overflow: clip; + justify-self: center; + } + .image { + position: relative; + display: inline-block; + } + .icons { + position: absolute; + bottom: 14px; + left: 8px; + display: flex; + gap: 4px; + z-index: 10; + } + .icon-wrapper { + position: relative; + width: 24px; + height: 24px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + .icon-wrapper::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background-color: white; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + .tooltip-container { + display: none; + flex-direction: column; + position: absolute; + top: 32px; + left: 0; /* Align with first icon */ + background-color: white; + color: black; + padding: 8px; + border-radius: 6px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 6px; + width: max-content; + z-index: 20; + } + .tooltip { + font-size: 12px; + padding: 4px 8px; + border-bottom: 1px solid #ccc; + text-align: left; + white-space: nowrap; + } + .tooltip:last-child { + border-bottom: none; + } + .icons:hover .tooltip-container { + display: block; + } + .tooltip-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-bottom: 1px solid #ccc; + } + + .tooltip-row:last-child { + border-bottom: none; + } + + .tooltip-icon { + width: 20px; + height: 20px; + } + h3 { + font-size: var(--ddd-font-size-4xs); + } + p { + font-size: var(--ddd-font-size-4xs); + padding: 0; + margin: 0; + } + a:link { + color: var(--ddd-theme-default-nittanyNavy, #001e44); + text-decoration: underline; + font-family: var(--ddd-font-primary, sans-serif); + font-size: var(--ddd-font-size-3xs, 11px); + font-weight: var(--ddd-font-weight-medium, 500); + transition: color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + } + + a:visited { + color: var(--ddd-theme-default-slateGray, #666); + } + + a:hover, + a:focus { + color: var(--ddd-theme-default-keystoneYellow, #ffd100); + text-decoration: none; + } + simple-icon-lite { + color: var(--ddd-theme-default-nittanyNavy, #001e44); + --simple-icon-width: var(--ddd-icon-4xs, 20px); + --simple-icon-height: var(--ddd-icon-4xs, 20px); + } + button { + display: flex; + background: var(--ddd-theme-default-nittanyNavy, #001e44); + color: var(--ddd-theme-default-white, white); + border: var(--ddd-border-xs, 1px solid) transparent; + border-radius: var(--ddd-radius-sm, 4px); + font-family: var(--ddd-font-primary, sans-serif); + font-size: var(--ddd-font-size-3xs, 11px); + font-weight: var(--ddd-font-weight-medium, 500); + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + margin: 0px var(--ddd-spacing-1, 4px); + min-height: var(--ddd-spacing-7, 28px); + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--ddd-boxShadow-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + button:focus, + button:hover { + background: var(--ddd-theme-default-keystoneYellow, #ffd100); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + transform: translateY(-1px); + box-shadow: var(--ddd-boxShadow-md); + } + .cardBottom { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 6px; + padding: 0px 12px 16px 12px; + gap: 4px; + } + + .cardBottom button, + .cardBottom a { + flex: 1; + margin: 0 2px; + min-width: 0; + font-size: var(--ddd-font-size-3xs, 11px); + } + + :host([isSelected]) button.select { + background-color: var(--ddd-theme-default-nittanyNavy); + } + .titleBar { + display: inline-flex; + flex-direction: column; + text-align: left; + padding: 0px var(--ddd-spacing-3, 12px); + } + .titleBar h3 { + margin: 0; + font-size: var(--ddd-font-size-4xs, 14px); + height: var(--ddd-spacing-6, 24px); + overflow: hidden; + } + .titleBar p { + font-size: var(--ddd-font-size-4xs, 12px); + line-height: 1.4; + height: var(--ddd-spacing-12, 48px); + overflow: hidden; + } + @media (max-width: 768px) { + :host { + margin: var(--ddd-spacing-1, 4px); + min-height: 200px; + width: 100%; + max-width: none; + } + .image img { + width: 100%; + max-width: none; + } + .cardContent { + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px) + var(--ddd-spacing-4, 16px); + } + .titleBar { + padding: 0px var(--ddd-spacing-3, 12px); + } + .cardBottom { + gap: var(--ddd-spacing-2, 8px); + padding: 0px var(--ddd-spacing-3, 12px) var(--ddd-spacing-4, 16px) + var(--ddd-spacing-3, 12px); + } + .cardBottom button, + .cardBottom a { + font-size: var(--ddd-font-size-3xs, 12px); + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + min-height: var(--ddd-spacing-8, 32px); + margin: 0; + } + h3 { + font-size: var(--ddd-font-size-s, 16px) !important; + margin: var(--ddd-spacing-2, 8px) 0; + } + p { + font-size: var(--ddd-font-size-xs, 14px); + line-height: 1.4; + } + } + + @media (min-width: 769px) { + :host, + .image img { + display: flex; + width: 220px; + } + :host .collapseFilter { + display: flex; + } + } + `, + ]; + } + + toggleDisplay() { + this.isSelected = !this.isSelected; + this.showContinue = this.isSelected; + + this.dispatchEvent( + new CustomEvent("toggle-display", { + detail: { isSelected: this.isSelected }, + bubbles: true, + composed: true, + }), + ); + + // If selected, immediately trigger the continue action to open modal + if (this.isSelected) { + setTimeout(() => { + this.continueAction(); + }, 100); // Small delay to allow state to update + } + } + + continueAction() { + this.dispatchEvent( + new CustomEvent("continue-action", { + detail: { + title: this.title, + description: this.description, + source: this.source, + template: this.title, // Using title as template identifier + }, + bubbles: true, + composed: true, + }), + ); + } + + openDemo() { + if (this.demoLink) { + globalThis.open(this.demoLink, "_blank"); + } + } + + render() { + return html` +
+
+ ${this.title} +
+ ${this.iconImage.map( + (icon) => html` +
+ +
+ `, + )} +
+ ${this.iconImage.map( + (icon) => html` +
+ +
${icon.tooltip}
+
+ `, + )} +
+
+
+
+

${this.title}

+

${this.description}

+
+
+ + ${!this.isSelected + ? html`` + : ""} +
+
+ `; + } +} +customElements.define(AppHaxUseCase.tag, AppHaxUseCase); diff --git a/elements/app-hax/lib/v2/app-hax-user-access-modal.js b/elements/app-hax/lib/v2/app-hax-user-access-modal.js new file mode 100644 index 0000000000..0b7e76dc74 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-user-access-modal.js @@ -0,0 +1,502 @@ +/** + * Copyright 2025 The Pennsylvania State University + * @license Apache-2.0, see License.md for full text. + */ +import { html, css } from "lit"; +import { DDD } from "@haxtheweb/d-d-d/d-d-d.js"; +import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; +import "@haxtheweb/rpg-character/rpg-character.js"; +import "@haxtheweb/simple-icon/lib/simple-icons.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-button.js"; +import { store } from "./AppHaxStore.js"; + +/** + * `app-hax-user-access-modal` + * `Modal for managing user access to HAXiam sites` + * + * @demo demo/index.html + * @element app-hax-user-access-modal + */ +class AppHaxUserAccessModal extends I18NMixin(DDD) { + /** + * Convention we use + */ + static get tag() { + return "app-hax-user-access-modal"; + } + + constructor() { + super(); + this.username = ""; + this.loading = false; + this.error = ""; + this.siteTitle = ""; + this.t = { + userAccess: "User Access", + enterUsername: "Enter username to grant access", + usernamePlaceholder: "Username", + addUser: "Add User", + cancel: "Cancel", + userAccessGranted: "User access granted successfully!", + userNotFound: "User not found or unauthorized", + loadingAddingUser: "Adding user...", + grantAccessTo: "Grant access to:", + }; + } + + static get properties() { + return { + ...super.properties, + /** + * Username to add + */ + username: { + type: String, + }, + /** + * Loading state + */ + loading: { + type: Boolean, + }, + /** + * Error message + */ + error: { + type: String, + }, + /** + * Current site title + */ + siteTitle: { + type: String, + }, + }; + } + + static get styles() { + return [ + super.styles, + css` + :host { + display: block; + font-family: var(--ddd-font-primary); + background-color: var(--ddd-theme-default-white); + border-radius: var(--ddd-radius-sm); + padding: var(--ddd-spacing-6); + min-width: 420px; + max-width: 500px; + } + + .modal-content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--ddd-spacing-4); + } + + .site-info { + text-align: center; + margin-bottom: var(--ddd-spacing-2); + } + + .site-title { + color: var(--ddd-theme-default-nittanyNavy); + font-weight: var(--ddd-font-weight-bold); + font-size: var(--ddd-font-size-s); + margin: var(--ddd-spacing-1) 0; + } + + .character-container { + display: flex; + justify-content: center; + margin: var(--ddd-spacing-2) 0; + padding: var(--ddd-spacing-3); + border-radius: var(--ddd-radius-sm); + background-color: var(--ddd-theme-default-limestoneLight); + } + + .input-container { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--ddd-spacing-2); + } + + input { + padding: var(--ddd-spacing-3); + border: var(--ddd-border-sm); + border-radius: var(--ddd-radius-xs); + font-family: var(--ddd-font-primary); + font-size: var(--ddd-font-size-s); + width: 100%; + box-sizing: border-box; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; + } + + input:focus { + outline: none; + border-color: var(--ddd-theme-default-nittanyNavy); + box-shadow: 0 0 0 2px var(--ddd-theme-default-potential30); + } + + .buttons { + display: flex; + gap: var(--ddd-spacing-3); + justify-content: center; + width: 100%; + margin-top: var(--ddd-spacing-3); + } + + button { + background: var(--ddd-theme-default-nittanyNavy, #001e44); + color: var(--ddd-theme-default-white, white); + border: none; + border-radius: var(--ddd-radius-sm); + padding: var(--ddd-spacing-3) var(--ddd-spacing-5); + font-family: var(--ddd-font-primary); + font-size: var(--ddd-font-size-s); + font-weight: var(--ddd-font-weight-medium); + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; + display: flex; + align-items: center; + justify-content: center; + } + + button:hover:not(:disabled) { + background: var(--ddd-theme-default-keystoneYellow, #ffd100); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + transform: translateY(-1px); + box-shadow: var(--ddd-boxShadow-sm); + } + + button:disabled { + background: var(--ddd-theme-default-slateGray); + cursor: not-allowed; + transform: none; + box-shadow: none; + opacity: 0.6; + } + + .cancel-button { + background: transparent; + color: var(--ddd-theme-default-nittanyNavy); + border: var(--ddd-border-sm); + } + + .cancel-button:hover:not(:disabled) { + background: var(--ddd-theme-default-slateLight); + color: var(--ddd-theme-default-nittanyNavy); + transform: translateY(-1px); + } + + .error { + color: var(--ddd-theme-default-original87Pink); + font-size: var(--ddd-font-size-xs); + text-align: center; + background-color: var(--ddd-theme-default-original87Pink10); + padding: var(--ddd-spacing-2); + border-radius: var(--ddd-radius-xs); + border: 1px solid var(--ddd-theme-default-original87Pink30); + } + + .loading { + display: flex; + align-items: center; + gap: var(--ddd-spacing-2); + justify-content: center; + } + + .loading simple-icon { + --simple-icon-width: 16px; + --simple-icon-height: 16px; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + h3 { + margin: 0; + color: var(--ddd-theme-default-nittanyNavy); + font-size: var(--ddd-font-size-l); + text-align: center; + font-weight: var(--ddd-font-weight-bold); + } + + p { + margin: 0; + color: var(--ddd-theme-default-coalyGray); + font-size: var(--ddd-font-size-s); + text-align: center; + line-height: 1.4; + } + + .empty-character { + display: flex; + align-items: center; + justify-content: center; + width: 120px; + height: 120px; + color: var(--ddd-theme-default-slateGray); + font-size: var(--ddd-font-size-xs); + text-align: center; + } + `, + ]; + } + + render() { + return html` + + `; + } + + /** + * Handle username input changes + */ + _handleUsernameInput(e) { + this.username = e.target.value; + // Clear error when user types + if (this.error) { + this.error = ""; + } + } + + /** + * Handle keydown events + */ + _handleKeydown(e) { + if (e.key === "Enter" && this.username.trim() && !this.loading) { + this._handleAddUser(); + } else if (e.key === "Escape") { + this._handleCancel(); + } + } + + /** + * Handle add user button click + */ + async _handleAddUser() { + if (!this.username.trim()) { + return; + } + + this.loading = true; + this.error = ""; + + try { + const response = await this._addUserAccess(this.username.trim()); + + if (response.ok) { + // Play success sound + if (store.appEl && store.appEl.playSound) { + store.appEl.playSound("success"); + } + // Success - show toast and close modal + this._showSuccessToast(); + this._closeModal(); + // Reset form + this.username = ""; + } else if (response.status === 403) { + // User not found or unauthorized + this.error = this.t.userNotFound; + } else { + // Other error + this.error = `Error: ${response.status} ${response.statusText}`; + } + } catch (error) { + console.error("Error adding user access:", error); + this.error = "Network error occurred. Please try again."; + } finally { + this.loading = false; + } + } + + /** + * Handle cancel button click + */ + _handleCancel() { + // Removed sound effects for modal close/cancel as requested + this._closeModal(); + } + + /** + * Add user access via HAXiam API + */ + async _addUserAccess(username) { + // Get the site name from the store - this should be the directory name + let siteName = null; + if (store.activeSite && store.activeSite.name) { + siteName = store.activeSite.name; + } else if ( + store.activeSite && + store.activeSite.metadata && + store.activeSite.metadata.site && + store.activeSite.metadata.site.name + ) { + siteName = store.activeSite.metadata.site.name; + } + + if (!siteName) { + throw new Error("Unable to determine site name"); + } + + // Use the secure AppHaxAPI.makeCall method with proper token validation + const response = await store.AppHaxAPI.makeCall("haxiamAddUserAccess", { + userName: username, + siteName: siteName, + }); + + // Convert to fetch-like response object for compatibility with existing error handling + return { + ok: !response.__failed, + status: response.__failed ? response.__failed.status : 200, + statusText: response.__failed ? response.__failed.message : "OK", + data: response, + }; + } + + /** + * Show success toast with RPG character matching the added user + */ + _showSuccessToast() { + // Use the existing toast system but with the character seed matching the added user + store.toast(this.t.userAccessGranted, 4000, { + hat: "construction", + userName: this.username, // This ensures the toast character matches the user we just added + }); + } + + /** + * Close the modal + */ + _closeModal() { + // Restore body scrolling + document.body.style.overflow = ""; + + globalThis.dispatchEvent( + new CustomEvent("simple-modal-hide", { + bubbles: true, + composed: true, + cancelable: true, + detail: {}, + }), + ); + } + + /** + * Focus input when modal opens + */ + firstUpdated() { + super.firstUpdated(); + // Set site title from store if available + if (store.activeSite && store.activeSite.title) { + this.siteTitle = store.activeSite.title; + } + + // Focus input after a brief delay + setTimeout(() => { + const input = this.shadowRoot.querySelector("input"); + if (input) { + input.focus(); + } + }, 100); + } + + /** + * Reset form when modal opens + */ + connectedCallback() { + super.connectedCallback(); + this.username = ""; + this.error = ""; + this.loading = false; + + // Prevent body scrolling when modal is connected/opened + document.body.style.overflow = "hidden"; + } +} + +globalThis.customElements.define( + AppHaxUserAccessModal.tag, + AppHaxUserAccessModal, +); +export { AppHaxUserAccessModal }; diff --git a/elements/app-hax/lib/v2/app-hax-user-menu-button.js b/elements/app-hax/lib/v2/app-hax-user-menu-button.js new file mode 100644 index 0000000000..301fa174c9 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-user-menu-button.js @@ -0,0 +1,129 @@ +// TODO: Text-overflow-ellipses + +// dependencies / things imported +import { LitElement, html, css } from "lit"; +import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-lite.js"; + +export class AppHaxUserMenuButton extends DDDSuper(LitElement) { + // a convention I enjoy so you can change the tag name in 1 place + static get tag() { + return "app-hax-user-menu-button"; + } + + constructor() { + super(); + this.icon = "account-circle"; + this.label = "Default"; + } + + handleClick(e) { + // Find the parent anchor element and trigger its click + const parentAnchor = this.parentElement; + if (parentAnchor && parentAnchor.tagName.toLowerCase() === "a") { + e.stopPropagation(); + parentAnchor.click(); + } + } + + static get properties() { + return { + icon: { type: String }, + label: { type: String }, + }; + } + + static get styles() { + return [ + super.styles, + css` + :host { + font-family: var(--ddd-font-primary, sans-serif); + text-align: center; + width: 100%; + display: block; + } + + .menu-button { + display: flex; + align-items: center; + width: 100%; + border: none; + margin: 0; + padding: var(--ddd-spacing-2, 8px) var(--ddd-spacing-3, 12px); + font-size: var(--ddd-font-size-3xs, 12px); + text-align: left; + color: var(--ddd-theme-default-nittanyNavy, #001e44); + background: transparent; + cursor: pointer; + font-family: var(--ddd-font-primary, sans-serif); + transition: all 0.2s ease; + min-height: var(--ddd-spacing-8, 32px); + box-sizing: border-box; + } + + :host([dark]) .menu-button, + body.dark-mode .menu-button { + color: var(--ddd-theme-default-white, white); + } + + .menu-button:hover, + .menu-button:active, + .menu-button:focus { + background: var(--ddd-theme-default-limestoneGray, #f5f5f5); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + outline: none; + } + + :host([dark]) .menu-button:hover, + :host([dark]) .menu-button:active, + :host([dark]) .menu-button:focus, + body.dark-mode .menu-button:hover, + body.dark-mode .menu-button:active, + body.dark-mode .menu-button:focus { + background: var(--ddd-theme-default-slateGray, #666); + color: var(--ddd-theme-default-white, white); + } + + :host(.logout) .menu-button:hover, + :host(.logout) .menu-button:active, + :host(.logout) .menu-button:focus { + background: var(--ddd-theme-default-original87Pink, #e4007c); + color: var(--ddd-theme-default-white, white); + } + + .icon { + padding-right: var(--ddd-spacing-2, 8px); + font-size: var(--ddd-font-size-xs, 14px); + flex-shrink: 0; + display: flex; + align-items: center; + } + + .label { + flex: 1; + text-align: left; + } + `, + ]; + } + + render() { + return html` + + `; + } +} +customElements.define(AppHaxUserMenuButton.tag, AppHaxUserMenuButton); diff --git a/elements/app-hax/lib/v2/app-hax-user-menu.js b/elements/app-hax/lib/v2/app-hax-user-menu.js new file mode 100644 index 0000000000..f2f8b3e6d5 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-user-menu.js @@ -0,0 +1,338 @@ +// TODO: Create app-hax-user-menu-button to be tossed into this +// TODO: Create prefix and suffix sections for sound/light toggles and other shtuff + +// dependencies / things imported +import { LitElement, html, css } from "lit"; +import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; +import "@haxtheweb/simple-icon/lib/simple-icon-lite.js"; + +export class AppHaxUserMenu extends DDDSuper(LitElement) { + // a convention I enjoy so you can change the tag name in 1 place + static get tag() { + return "app-hax-user-menu"; + } + + constructor() { + super(); + this.isOpen = false; + this.icon = "account-circle"; + this.addEventListener("keydown", this._handleKeydown.bind(this)); + } + + static get properties() { + return { + isOpen: { type: Boolean, reflect: true, attribute: "is-open" }, + icon: { type: String, reflect: true }, + }; + } + + static get styles() { + return [ + super.styles, + css` + :host { + font-family: var(--ddd-font-primary, sans-serif); + text-align: center; + display: inline-block; + margin: 0px; + padding: 0px; + position: relative; + } + + .entireComponent { + max-height: var(--ddd-spacing-10, 40px); + } + + .menuToggle { + cursor: pointer; + max-height: var(--ddd-spacing-10, 40px); + } + + .user-menu { + display: none; + } + + .user-menu.open { + display: block; + top: var(--ddd-spacing-12, 48px); + right: 0px; + position: absolute; + border: var(--ddd-border-xs, 1px solid) + var(--ddd-theme-default-slateGray, #666); + background: var(--ddd-theme-default-white, white); + border-radius: var(--ddd-radius-sm, 4px); + box-shadow: var(--ddd-boxShadow-lg); + min-width: var(--ddd-spacing-30, 200px); + z-index: 1000; + overflow: hidden; + } + + :host([dark]) .user-menu.open, + body.dark-mode .user-menu.open { + background: var(--ddd-theme-default-coalyGray, #333); + border-color: var(--ddd-theme-default-slateGray, #666); + } + + .user-menu.open ::slotted(*) { + display: block; + width: 100%; + margin: 0; + font-size: var(--ddd-font-size-3xs, 12px); + text-align: left; + font-family: var(--ddd-font-primary, sans-serif); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + background: transparent; + text-decoration: none; + } + + :host([dark]) .user-menu.open ::slotted(*), + body.dark-mode .user-menu.open ::slotted(*) { + color: var(--ddd-theme-default-white, white); + } + + .user-menu.open .main-menu ::slotted(*:hover), + .user-menu.open .main-menu ::slotted(*:active), + .user-menu.open .main-menu ::slotted(*:focus) { + background: var(--ddd-theme-default-limestoneGray, #f5f5f5); + color: var(--ddd-theme-default-nittanyNavy, #001e44); + } + + :host([dark]) .user-menu.open .main-menu ::slotted(*:hover), + :host([dark]) .user-menu.open .main-menu ::slotted(*:active), + :host([dark]) .user-menu.open .main-menu ::slotted(*:focus), + body.dark-mode .user-menu.open .main-menu ::slotted(*:hover), + body.dark-mode .user-menu.open .main-menu ::slotted(*:active), + body.dark-mode .user-menu.open .main-menu ::slotted(*:focus) { + background: var(--ddd-theme-default-slateGray, #666); + color: var(--ddd-theme-default-white, white); + } + + .user-menu.open .post-menu ::slotted(*:hover), + .user-menu.open .post-menu ::slotted(*:active), + .user-menu.open .post-menu ::slotted(*:focus) { + background: var(--ddd-theme-default-original87Pink, #e4007c); + color: var(--ddd-theme-default-white, white); + } + + .user-menu ::slotted(button) { + cursor: pointer; + } + + .user-menu ::slotted(*) simple-icon-lite { + padding-right: var(--ddd-spacing-2, 8px); + } + + .pre-menu, + .post-menu { + border-top: var(--ddd-border-xs, 1px solid) + var(--ddd-theme-default-limestoneGray, #f5f5f5); + } + + .pre-menu:first-child, + .main-menu:first-child { + border-top: none; + } + + /* Keyboard focus indicators */ + .user-menu ::slotted(*:focus), + .user-menu ::slotted(*[tabindex="0"]:focus) { + outline: var(--ddd-border-sm, 2px solid) + var(--ddd-theme-default-keystoneYellow, #ffd100); + outline-offset: -2px; + } + `, + ]; + } + + render() { + return html` +
+ + + +
+ `; + } + + /** + * Handle keyboard navigation for menu toggle + */ + _handleMenuToggleKeydown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this._toggleMenu(); + } else if (e.key === "Escape" && this.isOpen) { + e.preventDefault(); + this._closeMenu(); + } + } + + /** + * Handle keyboard navigation within menu + */ + _handleKeydown(e) { + if (!this.isOpen) return; + + const menuItems = this._getMenuItems(); + const currentIndex = this._getCurrentMenuItemIndex(menuItems); + + switch (e.key) { + case "Escape": + e.preventDefault(); + this._closeMenu(); + this._focusToggle(); + break; + case "ArrowDown": + e.preventDefault(); + this._focusNextItem(menuItems, currentIndex); + break; + case "ArrowUp": + e.preventDefault(); + this._focusPreviousItem(menuItems, currentIndex); + break; + case "Home": + e.preventDefault(); + this._focusFirstItem(menuItems); + break; + case "End": + e.preventDefault(); + this._focusLastItem(menuItems); + break; + } + } + + /** + * Toggle menu open/closed state + */ + _toggleMenu() { + this.isOpen = !this.isOpen; + if (this.isOpen) { + // Focus first menu item when opening + setTimeout(() => { + const menuItems = this._getMenuItems(); + if (menuItems.length > 0) { + menuItems[0].focus(); + } + }, 0); + } + } + + /** + * Close menu and restore focus + */ + _closeMenu() { + this.isOpen = false; + } + + /** + * Focus the menu toggle button + */ + _focusToggle() { + const toggle = this.shadowRoot.querySelector(".menuToggle"); + if (toggle) { + toggle.focus(); + } + } + + /** + * Get all focusable menu items + */ + _getMenuItems() { + const menu = this.shadowRoot.querySelector(".user-menu"); + if (!menu) return []; + + const items = menu.querySelectorAll("slot"); + const menuItems = []; + + items.forEach((slot) => { + const assignedElements = slot.assignedElements(); + assignedElements.forEach((el) => { + // Find focusable elements within slotted content + const focusable = el.matches('a, button, [tabindex="0"]') + ? [el] + : el.querySelectorAll('a, button, [tabindex="0"]'); + menuItems.push(...focusable); + }); + }); + + return menuItems; + } + + /** + * Get current focused menu item index + */ + _getCurrentMenuItemIndex(menuItems) { + const activeElement = + this.shadowRoot.activeElement || document.activeElement; + return menuItems.indexOf(activeElement); + } + + /** + * Focus next menu item + */ + _focusNextItem(menuItems, currentIndex) { + const nextIndex = + currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0; + if (menuItems[nextIndex]) { + menuItems[nextIndex].focus(); + } + } + + /** + * Focus previous menu item + */ + _focusPreviousItem(menuItems, currentIndex) { + const prevIndex = + currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1; + if (menuItems[prevIndex]) { + menuItems[prevIndex].focus(); + } + } + + /** + * Focus first menu item + */ + _focusFirstItem(menuItems) { + if (menuItems[0]) { + menuItems[0].focus(); + } + } + + /** + * Focus last menu item + */ + _focusLastItem(menuItems) { + if (menuItems[menuItems.length - 1]) { + menuItems[menuItems.length - 1].focus(); + } + } +} +customElements.define(AppHaxUserMenu.tag, AppHaxUserMenu); diff --git a/elements/app-hax/lib/v2/app-hax-wired-toggle.js b/elements/app-hax/lib/v2/app-hax-wired-toggle.js new file mode 100644 index 0000000000..16ebeb6b57 --- /dev/null +++ b/elements/app-hax/lib/v2/app-hax-wired-toggle.js @@ -0,0 +1,99 @@ +import { autorun, toJS } from "mobx"; +import { html, css } from "lit"; +import { store } from "./AppHaxStore.js"; +import { WiredDarkmodeToggle } from "@haxtheweb/haxcms-elements/lib/core/ui/wired-darkmode-toggle/wired-darkmode-toggle.js"; +import { SimpleTourFinder } from "@haxtheweb/simple-popover/lib/SimpleTourFinder.js"; + +export class AppHAXWiredToggle extends SimpleTourFinder(WiredDarkmodeToggle) { + constructor() { + super(); + this.tourName = "hax"; + // Create a media query to monitor platform color scheme changes + this.darkModeMediaQuery = globalThis.matchMedia( + "(prefers-color-scheme: dark)", + ); + + // Function to handle both autorun updates and media query changes + this._updateToggleState = () => { + this.checked = toJS(store.darkMode); + // Disable toggle when platform is in dark mode, preventing switch to light mode + if (this.darkModeMediaQuery.matches) { + this.disabled = true; + } else { + this.disabled = false; + } + }; + + // Set up autorun for store changes + autorun(this._updateToggleState); + + // Listen for platform color scheme changes + this.darkModeMediaQuery.addEventListener("change", this._updateToggleState); + } + + static get tag() { + return "app-hax-wired-toggle"; + } + + disconnectedCallback() { + super.disconnectedCallback(); + // Clean up media query event listener + if (this.darkModeMediaQuery && this._updateToggleState) { + this.darkModeMediaQuery.removeEventListener( + "change", + this._updateToggleState, + ); + } + } + + updated(changedProperties) { + if (super.updated) { + super.updated(changedProperties); + } + changedProperties.forEach((oldValue, propName) => { + if (propName === "checked" && oldValue !== undefined) { + store.darkMode = this[propName]; + } + }); + } + + static get styles() { + return [ + super.styles, + css` + /* Screen reader only text */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + `, + ]; + } + render() { + return html` +
+ ${super.render()} +
+ Toggle between light and dark mode themes +
+
+ You can toggle your user interface between "light" and "dark" for your + viewing enjoyment. +
+
+ `; + } +} +customElements.define(AppHAXWiredToggle.tag, AppHAXWiredToggle); diff --git a/elements/app-hax/lib/v2/skeleton-loader.js b/elements/app-hax/lib/v2/skeleton-loader.js new file mode 100644 index 0000000000..4a64dd5766 --- /dev/null +++ b/elements/app-hax/lib/v2/skeleton-loader.js @@ -0,0 +1,191 @@ +/** + * Skeleton Loader Utility + * + * Handles loading and processing skeleton files for the app-hax v2 system. + * Since skeleton files now match the createSite API format directly, + * this utility focuses on loading, validation, and metadata extraction. + */ + +export class SkeletonLoader { + /** + * Load skeleton from JSON data and prepare for app-hax usage + * @param {Object} skeletonData - Raw skeleton JSON data + * @param {Object} options - Loading options + * @returns {Object} Processed skeleton ready for app-hax + */ + static loadSkeleton(skeletonData, options = {}) { + // Validate skeleton format + if (!this.isValidSkeleton(skeletonData)) { + throw new Error("Invalid skeleton format"); + } + + // Extract metadata for app-hax use case display + const useCaseData = { + dataType: "skeleton", + useCaseTitle: skeletonData.meta.useCaseTitle || skeletonData.meta.name, + useCaseDescription: + skeletonData.meta.useCaseDescription || skeletonData.meta.description, + useCaseImage: + skeletonData.meta.useCaseImage || options.defaultImage || "", + useCaseIcon: options.defaultIcons || [ + { icon: "hax:site", tooltip: "Site Template" }, + ], + useCaseTag: this.extractTags(skeletonData), + demoLink: skeletonData.meta.sourceUrl || "#", + originalData: skeletonData, + + // App-hax specific properties + isSelected: false, + showContinue: false, + }; + + return { + // Use case display data + ...useCaseData, + + // Direct createSite API data (no transformation needed) + createSiteData: { + site: skeletonData.site, + build: skeletonData.build, + theme: skeletonData.theme, + }, + }; + } + + /** + * Extract tags for filtering from skeleton + * @param {Object} skeleton - Skeleton data + * @returns {Array} Tags array + */ + static extractTags(skeleton) { + const tags = []; + + // Add categories and tags from meta + if (skeleton.meta.category && Array.isArray(skeleton.meta.category)) { + tags.push(...skeleton.meta.category); + } + if (skeleton.meta.tags && Array.isArray(skeleton.meta.tags)) { + tags.push(...skeleton.meta.tags); + } + + // Add build type as tag + if (skeleton.build && skeleton.build.type) { + tags.push(skeleton.build.type); + } + + // Add theme as tag + if (skeleton.site && skeleton.site.theme) { + tags.push(`theme-${skeleton.site.theme}`); + } + + // Deduplicate and filter empty + return [...new Set(tags.filter((tag) => tag && tag.trim() !== ""))]; + } + + /** + * Validate skeleton format + * @param {Object} skeleton - Skeleton to validate + * @returns {boolean} Is valid skeleton + */ + static isValidSkeleton(skeleton) { + if (!skeleton || typeof skeleton !== "object") return false; + + // Check required top-level structure + if (!skeleton.meta || skeleton.meta.type !== "skeleton") return false; + if (!skeleton.site || !skeleton.build) return false; + + // Check site structure + if (!skeleton.site.name || !skeleton.site.theme) return false; + + // Check build structure + if (!skeleton.build.items || !Array.isArray(skeleton.build.items)) + return false; + + return true; + } + + /** + * Load multiple skeletons from an array of skeleton data + * @param {Array} skeletonArray - Array of skeleton JSON objects + * @param {Object} options - Loading options + * @returns {Array} Array of processed skeletons + */ + static loadSkeletons(skeletonArray, options = {}) { + if (!Array.isArray(skeletonArray)) { + throw new Error("Expected array of skeletons"); + } + + return skeletonArray + .map((skeleton, index) => { + try { + return this.loadSkeleton(skeleton, { + ...options, + index, + }); + } catch (error) { + console.warn(`Failed to load skeleton at index ${index}:`, error); + return null; + } + }) + .filter(Boolean); // Remove failed loads + } + + /** + * Apply user customizations to skeleton createSite data + * @param {Object} skeleton - Processed skeleton from loadSkeleton() + * @param {Object} customizations - User customizations + * @returns {Object} CreateSite data with customizations applied + */ + static applyCustomizations(skeleton, customizations = {}) { + const createSiteData = JSON.parse(JSON.stringify(skeleton.createSiteData)); // Deep clone + + // Apply site customizations + if (customizations.siteName) { + createSiteData.site.name = customizations.siteName; + } + if (customizations.siteDescription) { + createSiteData.site.description = customizations.siteDescription; + } + if (customizations.theme) { + createSiteData.site.theme = customizations.theme; + } + + // Apply theme customizations + if (customizations.color) { + createSiteData.theme.color = customizations.color; + } + if (customizations.icon) { + createSiteData.theme.icon = customizations.icon; + } + + // Apply any additional theme settings + if (customizations.themeSettings) { + Object.assign(createSiteData.theme, customizations.themeSettings); + } + + return createSiteData; + } + + /** + * Generate skeleton metadata for directory processing + * @param {Object} skeleton - Skeleton data + * @returns {Object} Metadata for backend use + */ + static generateSkeletonMetadata(skeleton) { + return { + name: skeleton.meta.name, + title: skeleton.meta.useCaseTitle || skeleton.meta.name, + description: + skeleton.meta.useCaseDescription || skeleton.meta.description, + type: skeleton.build.type || "skeleton", + theme: skeleton.site.theme, + itemCount: skeleton.build.items ? skeleton.build.items.length : 0, + fileCount: skeleton.build.files ? skeleton.build.files.length : 0, + created: skeleton.meta.created, + tags: this.extractTags(skeleton), + sourceUrl: skeleton.meta.sourceUrl || null, + }; + } +} + +export default SkeletonLoader; diff --git a/elements/app-hax/lib/v2/skeleton-uuid-manager.js b/elements/app-hax/lib/v2/skeleton-uuid-manager.js new file mode 100644 index 0000000000..d2f255ee80 --- /dev/null +++ b/elements/app-hax/lib/v2/skeleton-uuid-manager.js @@ -0,0 +1,128 @@ +/** + * UUID Manager for Skeleton Generation + * Handles UUID generation, mapping, and rewriting for skeleton templates + */ + +export class SkeletonUuidManager { + constructor() { + this.uuidMap = new Map(); + this.usedUuids = new Set(); + } + + /** + * Generate a new UUID v4 + * @returns {string} UUID + */ + generateUuid() { + // Simple UUID v4 generation without crypto dependency + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ); + } + + /** + * Get or create UUID for an item + * @param {string} originalId - Original item ID + * @returns {string} UUID + */ + getUuidForItem(originalId) { + if (this.uuidMap.has(originalId)) { + return this.uuidMap.get(originalId); + } + + let newUuid; + do { + newUuid = this.generateUuid(); + } while (this.usedUuids.has(newUuid)); + + this.uuidMap.set(originalId, newUuid); + this.usedUuids.add(newUuid); + return newUuid; + } + + /** + * Create mapping for parent-child relationships + * @param {Array} items - Site items + * @returns {Object} UUID mapping with relationships + */ + createRelationshipMap(items) { + const relationships = {}; + + items.forEach((item) => { + const itemUuid = this.getUuidForItem(item.id); + relationships[itemUuid] = { + originalId: item.id, + uuid: itemUuid, + parentUuid: item.parent ? this.getUuidForItem(item.parent) : null, + children: [], + }; + }); + + // Build children arrays + Object.values(relationships).forEach((item) => { + if (item.parentUuid && relationships[item.parentUuid]) { + relationships[item.parentUuid].children.push(item.uuid); + } + }); + + return relationships; + } + + /** + * Rewrite UUIDs in skeleton data for new site creation + * @param {Object} skeleton - Skeleton data + * @returns {Object} Skeleton with new UUIDs + */ + rewriteSkeletonUuids(skeleton) { + const newManager = new SkeletonUuidManager(); + const oldToNewMap = new Map(); + + // Create new UUIDs for all items + skeleton.structure.forEach((item) => { + if (item.uuid) { + const newUuid = newManager.generateUuid(); + oldToNewMap.set(item.uuid, newUuid); + } + }); + + // Rewrite structure with new UUIDs + const newStructure = skeleton.structure.map((item) => { + const newItem = { ...item }; + + if (item.uuid) { + newItem.uuid = oldToNewMap.get(item.uuid); + } + + if (item.parentUuid && oldToNewMap.has(item.parentUuid)) { + newItem.parentUuid = oldToNewMap.get(item.parentUuid); + } + + return newItem; + }); + + return { + ...skeleton, + structure: newStructure, + meta: { + ...skeleton.meta, + created: new Date().toISOString(), + sourceUuids: "rewritten", + }, + }; + } + + /** + * Reset the manager + */ + reset() { + this.uuidMap.clear(); + this.usedUuids.clear(); + } +} + +export default SkeletonUuidManager; diff --git a/elements/app-hax/lib/wired-darkmode-toggle/images/moonIcon.png b/elements/app-hax/lib/wired-darkmode-toggle/images/moonIcon.png new file mode 100644 index 0000000000..88eb079435 Binary files /dev/null and b/elements/app-hax/lib/wired-darkmode-toggle/images/moonIcon.png differ diff --git a/elements/app-hax/lib/wired-darkmode-toggle/images/sunIcon.png b/elements/app-hax/lib/wired-darkmode-toggle/images/sunIcon.png new file mode 100644 index 0000000000..6b94b10568 Binary files /dev/null and b/elements/app-hax/lib/wired-darkmode-toggle/images/sunIcon.png differ diff --git a/elements/app-hax/lib/wired-darkmode-toggle/wired-darkmode-toggle.js b/elements/app-hax/lib/wired-darkmode-toggle/wired-darkmode-toggle.js index 74b4f72539..d7aa691ce3 100644 --- a/elements/app-hax/lib/wired-darkmode-toggle/wired-darkmode-toggle.js +++ b/elements/app-hax/lib/wired-darkmode-toggle/wired-darkmode-toggle.js @@ -8,14 +8,15 @@ import { WiredToggle } from "wired-elements/lib/wired-toggle.js"; import { html, css, unsafeCSS } from "lit"; // need to highjack in order to alter the scale so we can fit our icon // for states -const sun = new URL("./images/sun.svg", import.meta.url).href; -const moon = new URL("./images/moon.svg", import.meta.url).href; +const sun = new URL("./images/sunIcon.png", import.meta.url).href; +const moon = new URL("./images/moonIcon.png", import.meta.url).href; export class WiredDarkmodeToggle extends WiredToggle { constructor() { super(); this.checked = false; this.label = "Dark mode"; + this.knobFill = svgNode("circle"); } // eslint-disable-next-line class-methods-use-this @@ -28,17 +29,44 @@ export class WiredDarkmodeToggle extends WiredToggle { } draw(svg, size) { - const rect = rectangle(svg, 0, 0, size[0], 48, this.seed); - rect.classList.add("toggle-bar"); + //const rect = rectangle(svg, 0, 0, size[0], 48, this.seed); + //rect.classList.add("toggle-bar"); this.knob = svgNode("g"); this.knob.classList.add("knob"); svg.appendChild(this.knob); - const knobFill = hachureEllipseFill(26, 26, 40, 40, this.seed); - knobFill.classList.add("knobfill"); - this.knob.appendChild(knobFill); + + this.knobFill.setAttribute("cx", 26); + this.knobFill.setAttribute("cy", 26); + this.knobFill.setAttribute("r", 20); + this.knobFill.setAttribute( + "style", + "fill: var(--wired-toggle-off-color); transition: fill 0.3s ease-in-out;", + ); + this.knobFill.classList.add("knobfill"); + this.knob.appendChild(this.knobFill); ellipse(this.knob, 26, 26, 40, 40, this.seed); } + toggleMode(checked) { + if (checked) { + this.knobFill.setAttribute( + "style", + "fill: var(--wired-toggle-on-color);", + ); + } else { + this.knobFill.setAttribute( + "style", + "fill: var(--wired-toggle-off-color);", + ); + } + } + + onChange(event) { + this.checked = event.target.checked; + this.toggleMode(this.checked); + this.dispatchEvent(new Event("change", { bubbles: true, composed: true })); + } + static get properties() { return { checked: { @@ -58,7 +86,7 @@ export class WiredDarkmodeToggle extends WiredToggle { render() { return html`
- + AudioPlayer: audio-player Demo - - +
diff --git a/elements/audio-player/index.html b/elements/audio-player/index.html index d32ffdbf86..2ea52ab23e 100644 --- a/elements/audio-player/index.html +++ b/elements/audio-player/index.html @@ -4,10 +4,10 @@ audio-player documentation - - + + - + diff --git a/elements/audio-player/package.json b/elements/audio-player/package.json index a2376115ca..d466fe1f93 100644 --- a/elements/audio-player/package.json +++ b/elements/audio-player/package.json @@ -44,16 +44,13 @@ "license": "Apache-2.0", "dependencies": { "@haxtheweb/video-player": "^11.0.5", - "lit": "3.3.0" + "lit": "3.3.1" }, "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", "@open-wc/testing": "4.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "9.1.2", "wct-browser-legacy": "1.0.2" }, diff --git a/elements/audio-player/test/audio-player.test.js b/elements/audio-player/test/audio-player.test.js index 87944f6c5a..9f205ef786 100644 --- a/elements/audio-player/test/audio-player.test.js +++ b/elements/audio-player/test/audio-player.test.js @@ -30,12 +30,14 @@ describe("audio-player test", () => { it("should extend VideoPlayer", () => { expect(element.constructor.name).to.equal("AudioPlayer"); - expect(element).to.be.instanceOf(globalThis.customElements.get('video-player') || Object); + expect(element).to.be.instanceOf( + globalThis.customElements.get("video-player") || Object, + ); }); it("should always return audioOnly as true", () => { expect(element.audioOnly).to.equal(true); - + // Test that it's always true regardless of attempts to change it element.audioOnly = false; expect(element.audioOnly).to.equal(true); @@ -46,9 +48,7 @@ describe("audio-player test", () => { let testElement; beforeEach(async () => { - testElement = await fixture(html` - - `); + testElement = await fixture(html` `); await testElement.updateComplete; }); @@ -84,19 +84,29 @@ describe("audio-player test", () => { it("should handle thumbnailSrc property", async () => { testElement.thumbnailSrc = "https://example.com/thumbnail.jpg"; await testElement.updateComplete; - expect(testElement.thumbnailSrc).to.equal("https://example.com/thumbnail.jpg"); + expect(testElement.thumbnailSrc).to.equal( + "https://example.com/thumbnail.jpg", + ); await expect(testElement).shadowDom.to.be.accessible(); }); it("should handle boolean properties", async () => { const booleanProps = [ - 'learningMode', 'hideYoutubeLink', 'linkable', - 'allowBackgroundPlay', 'darkTranscript', 'disableInteractive', - 'hideTimestamps', 'hideTranscript' + "learningMode", + "hideYoutubeLink", + "linkable", + "allowBackgroundPlay", + "darkTranscript", + "disableInteractive", + "hideTimestamps", + "hideTranscript", ]; for (const prop of booleanProps) { - if (testElement.hasOwnProperty(prop) || prop in testElement.constructor.properties) { + if ( + testElement.hasOwnProperty(prop) || + prop in testElement.constructor.properties + ) { testElement[prop] = true; await testElement.updateComplete; expect(testElement[prop]).to.equal(true); @@ -111,8 +121,8 @@ describe("audio-player test", () => { }); it("should handle crossorigin property", async () => { - const crossOriginValues = ['', 'anonymous', 'use-credentials']; - + const crossOriginValues = ["", "anonymous", "use-credentials"]; + for (const value of crossOriginValues) { testElement.crossorigin = value; await testElement.updateComplete; @@ -136,39 +146,39 @@ describe("audio-player test", () => { `); await testElement.updateComplete; - + expect(testElement.audioOnly).to.be.true; await expect(testElement).shadowDom.to.be.accessible(); }); it("should handle audio sources correctly", async () => { const testElement = await fixture(html` - `); await testElement.updateComplete; - - expect(testElement.source).to.include('.mp3'); - expect(testElement.mediaTitle).to.equal('Audio Test'); + + expect(testElement.source).to.include(".mp3"); + expect(testElement.mediaTitle).to.equal("Audio Test"); await expect(testElement).shadowDom.to.be.accessible(); }); it("should support different audio formats", async () => { const audioFormats = [ - 'https://example.com/audio.mp3', - 'https://example.com/audio.wav', - 'https://example.com/audio.ogg', - 'https://example.com/audio.m4a' + "https://example.com/audio.mp3", + "https://example.com/audio.wav", + "https://example.com/audio.ogg", + "https://example.com/audio.m4a", ]; - + for (const format of audioFormats) { const testElement = await fixture(html` `); await testElement.updateComplete; - + expect(testElement.source).to.equal(format); expect(testElement.audioOnly).to.be.true; await expect(testElement).shadowDom.to.be.accessible(); @@ -249,8 +259,8 @@ describe("audio-player test", () => { it("should handle audio file types correctly", () => { const haxProps = element.constructor.haxProperties; const handles = haxProps.gizmo.handles; - - const audioHandle = handles.find(handle => handle.type === "audio"); + + const audioHandle = handles.find((handle) => handle.type === "audio"); expect(audioHandle).to.exist; expect(audioHandle.type_exclusive).to.be.true; expect(audioHandle.source).to.equal("source"); @@ -259,26 +269,30 @@ describe("audio-player test", () => { it("should have proper HAX settings configuration", () => { const haxProps = element.constructor.haxProperties; const configItems = haxProps.settings.configure; - + // Verify source property configuration - const sourceProp = configItems.find(item => item.property === "source"); + const sourceProp = configItems.find((item) => item.property === "source"); expect(sourceProp).to.exist; expect(sourceProp.inputMethod).to.equal("haxupload"); expect(sourceProp.noCamera).to.be.true; expect(sourceProp.validationType).to.equal("url"); - + // Verify mediaTitle property - const titleProp = configItems.find(item => item.property === "mediaTitle"); + const titleProp = configItems.find( + (item) => item.property === "mediaTitle", + ); expect(titleProp).to.exist; expect(titleProp.inputMethod).to.equal("textfield"); - + // Verify accentColor property - const colorProp = configItems.find(item => item.property === "accentColor"); + const colorProp = configItems.find( + (item) => item.property === "accentColor", + ); expect(colorProp).to.exist; expect(colorProp.inputMethod).to.equal("colorpicker"); - + // Verify track property for captions - const trackProp = configItems.find(item => item.property === "track"); + const trackProp = configItems.find((item) => item.property === "track"); expect(trackProp).to.exist; expect(trackProp.noVoiceRecord).to.be.true; }); @@ -286,7 +300,7 @@ describe("audio-player test", () => { it("should maintain accessibility with HAX demo schema", async () => { const demoSchema = element.constructor.haxProperties.demoSchema[0]; const haxTestElement = await fixture(html` - { it("should remain accessible with invalid source URL", async () => { const testElement = await fixture(html` - @@ -328,7 +342,7 @@ describe("audio-player test", () => { it("should handle malformed caption files", async () => { const testElement = await fixture(html` - { it("should handle unusual property values", async () => { const testElement = await fixture(html``); - + const edgeCaseValues = [ " \t\n ", // whitespace "🎵 audio with emoji 🎶", // emoji "Very long audio title that might cause layout issues or other display problems", "Multi\nline\ntitle", // multiline - "Title with 'quotes' and \"double quotes\" and special chars: !@#$%^&*()" + "Title with 'quotes' and \"double quotes\" and special chars: !@#$%^&*()", ]; - + for (const value of edgeCaseValues) { testElement.mediaTitle = value; testElement.source = value; await testElement.updateComplete; - + expect(testElement.mediaTitle).to.equal(value); expect(testElement.source).to.equal(value); await expect(testElement).shadowDom.to.be.accessible(); @@ -366,15 +380,15 @@ describe("audio-player test", () => { const testElement = await fixture(html` `); - + // Test during initialization expect(testElement.audioOnly).to.be.true; - + // Test after update testElement.source = "https://example.com/another-audio.mp3"; await testElement.updateComplete; expect(testElement.audioOnly).to.be.true; - + // Test after property changes testElement.mediaTitle = "New Title"; testElement.accentColor = "purple"; @@ -384,13 +398,13 @@ describe("audio-player test", () => { it("should handle background playback settings", async () => { const testElement = await fixture(html` - `); await testElement.updateComplete; - + expect(testElement.allowBackgroundPlay).to.be.true; expect(testElement.audioOnly).to.be.true; await expect(testElement).shadowDom.to.be.accessible(); @@ -398,14 +412,14 @@ describe("audio-player test", () => { it("should support learning mode restrictions", async () => { const testElement = await fixture(html` - `); await testElement.updateComplete; - + expect(testElement.learningMode).to.be.true; expect(testElement.audioOnly).to.be.true; await expect(testElement).shadowDom.to.be.accessible(); @@ -414,22 +428,22 @@ describe("audio-player test", () => { describe("Constructor and inheritance behavior", () => { it("should call super() in constructor", () => { - const testElement = new (element.constructor)(); + const testElement = new element.constructor(); expect(testElement).to.be.instanceOf(element.constructor); }); it("should maintain audioOnly getter behavior", () => { - const testElement = new (element.constructor)(); - + const testElement = new element.constructor(); + // Test getter behavior expect(testElement.audioOnly).to.be.true; - + // Attempt to override (should still return true) - Object.defineProperty(testElement, 'audioOnly', { + Object.defineProperty(testElement, "audioOnly", { value: false, - writable: true + writable: true, }); - + // Getter should still return true expect(testElement.audioOnly).to.be.true; }); @@ -437,18 +451,25 @@ describe("audio-player test", () => { describe("Customization and theming", () => { it("should support custom accent colors", async () => { - const colors = ['red', 'blue', 'green', '#FF5733', 'rgb(255, 87, 51)', 'hsl(9, 100%, 60%)']; - + const colors = [ + "red", + "blue", + "green", + "#FF5733", + "rgb(255, 87, 51)", + "hsl(9, 100%, 60%)", + ]; + for (const color of colors) { const testElement = await fixture(html` - `); await testElement.updateComplete; - + expect(testElement.accentColor).to.equal(color); await expect(testElement).shadowDom.to.be.accessible(); } @@ -456,7 +477,7 @@ describe("audio-player test", () => { it("should support transcript customization", async () => { const testElement = await fixture(html` - { > `); await testElement.updateComplete; - + expect(testElement.darkTranscript).to.be.true; expect(testElement.hideTimestamps).to.be.true; expect(testElement.disableInteractive).to.be.true; diff --git a/elements/awesome-explosion/.gitignore b/elements/awesome-explosion/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/awesome-explosion/.gitignore +++ b/elements/awesome-explosion/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/awesome-explosion/awesome-explosion.js b/elements/awesome-explosion/awesome-explosion.js index 9696623237..239419bebb 100644 --- a/elements/awesome-explosion/awesome-explosion.js +++ b/elements/awesome-explosion/awesome-explosion.js @@ -3,6 +3,7 @@ * @license Apache-2.0, see License.md for full text. */ import { LitElement, html, css } from "lit"; +import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; /** * `awesome-explosion` * `An awesome, explosion.` @@ -11,55 +12,108 @@ import { LitElement, html, css } from "lit"; * @demo demo/index.html * @element awesome-explosion */ -class AwesomeExplosion extends LitElement { +class AwesomeExplosion extends DDDSuper(LitElement) { /** * LitElement constructable styles enhancement */ static get styles() { return [ + super.styles, css` :host { display: inline-block; + cursor: pointer; + transition: transform var(--ddd-duration-fast) var(--ddd-timing-ease); } + :host(:hover) { + transform: scale(1.05); + } + :host(:focus-visible) { + outline: var(--ddd-focus-ring); + outline-offset: var(--ddd-focus-offset); + } + + /* DDD-based sizing using icon variables */ :host([size="tiny"]) #image { - width: 80px; - height: 80px; + width: var(--ddd-icon-sm); + height: var(--ddd-icon-sm); } :host([size="small"]) #image { - width: 160px; - height: 160px; + width: var(--ddd-icon-lg); + height: var(--ddd-icon-lg); } :host([size="medium"]) #image { - width: 240px; - height: 240px; + width: var(--ddd-icon-xl); + height: var(--ddd-icon-xl); } :host([size="large"]) #image { - width: 320px; - height: 320px; + width: var(--ddd-icon-2xl); + height: var(--ddd-icon-2xl); } :host([size="epic"]) #image { - width: 720px; - height: 720px; + width: var(--ddd-icon-4xl); + height: var(--ddd-icon-4xl); } + /* DDD-based color theming with dark mode support */ :host([color="red"]) #image { - filter: sepia() saturate(10000%) hue-rotate(30deg); + filter: sepia() saturate(10000%) hue-rotate(30deg) brightness(0.9); } :host([color="purple"]) #image { - filter: sepia() saturate(10000%) hue-rotate(290deg); + filter: sepia() saturate(10000%) hue-rotate(290deg) brightness(0.9); } :host([color="blue"]) #image { - filter: sepia() saturate(10000%) hue-rotate(210deg); + filter: sepia() saturate(10000%) hue-rotate(210deg) brightness(0.9); } :host([color="orange"]) #image { - filter: sepia() saturate(10000%) hue-rotate(320deg); + filter: sepia() saturate(10000%) hue-rotate(320deg) brightness(0.9); } :host([color="yellow"]) #image { - filter: sepia() saturate(10000%) hue-rotate(70deg); + filter: sepia() saturate(10000%) hue-rotate(70deg) brightness(0.9); + } + + /* Dark mode adjustments */ + @media (prefers-color-scheme: dark) { + :host([color]) #image { + filter-brightness: 1.2; + } } + + /* Respect reduced motion preference */ + @media (prefers-reduced-motion: reduce) { + :host { + transition: none; + } + :host(:hover) { + transform: none; + } + #image { + animation: none !important; + } + } + #image { - width: 240px; - height: 240px; + width: var(--ddd-icon-xl); + height: var(--ddd-icon-xl); + border-radius: var(--ddd-radius-sm); + transition: filter var(--ddd-duration-fast) var(--ddd-timing-ease); + } + + /* Accessibility improvements */ + :host([disabled]) { + pointer-events: none; + opacity: var(--ddd-opacity-40); + } + + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } `, ]; @@ -75,10 +129,25 @@ class AwesomeExplosion extends LitElement { this.size = "medium"; this.color = ""; this.resetSound = false; + this.soundEnabled = true; + this.disabled = false; + this.tabIndex = 0; + + // Check for user preferences + this._checkUserPreferences(); + setTimeout(() => { - this.addEventListener("click", this._setPlaySound.bind(this)); - this.addEventListener("mouseover", this._setPlaySound.bind(this)); - this.addEventListener("mouseout", this._setStopSound.bind(this)); + this.addEventListener("click", this._handleClick.bind(this)); + this.addEventListener("keydown", this._handleKeydown.bind(this)); + + // Only add hover listeners if sound is enabled and motion is not reduced + if ( + this.soundEnabled && + !globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches + ) { + this.addEventListener("mouseover", this._handleMouseOver.bind(this)); + this.addEventListener("mouseout", this._handleMouseOut.bind(this)); + } }, 0); } render() { @@ -88,14 +157,30 @@ class AwesomeExplosion extends LitElement { src="${this.image}" id="image" class="image-tag" - alt="" + alt="${this._getAltText()}" + role="button" + aria-pressed="${this.playing ? "true" : "false"}" + aria-label="${this._getAriaLabel()}" /> + + ${this.soundEnabled + ? "Click or hover to play explosion sound" + : "Visual explosion effect (sound disabled)"} + `; } static get tag() { return "awesome-explosion"; } + + /** + * HAXSchema for proper integration with the HAX editor + */ + static get haxProperties() { + return new URL(`./lib/${this.tag}.haxProperties.json`, import.meta.url) + .href; + } updated(changedProperties) { changedProperties.forEach((oldValue, propName) => { if (propName == "state") { @@ -107,6 +192,7 @@ class AwesomeExplosion extends LitElement { } static get properties() { return { + ...super.properties, /** * State is for setting: * Possible values: play, pause, stop @@ -155,7 +241,7 @@ class AwesomeExplosion extends LitElement { }, /** * This is to change the color of the element. Possible values are: - * red, blue, orange, yellow + * red, blue, orange, yellow, purple */ color: { type: String, @@ -169,6 +255,21 @@ class AwesomeExplosion extends LitElement { reflect: true, attribute: "reset-sound", }, + /** + * Enable/disable sound effects globally + */ + soundEnabled: { + type: Boolean, + reflect: true, + attribute: "sound-enabled", + }, + /** + * Disable the entire component + */ + disabled: { + type: Boolean, + reflect: true, + }, }; } @@ -247,6 +348,93 @@ class AwesomeExplosion extends LitElement { } } + /** + * Handle click events with accessibility considerations + */ + _handleClick(e) { + if (this.disabled) return; + + if (this.state === "play") { + this.state = "stop"; + } else { + this.state = "play"; + } + } + + /** + * Handle keyboard interactions + */ + _handleKeydown(e) { + if (this.disabled) return; + + // Space or Enter key + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + this._handleClick(e); + } + } + + /** + * Handle mouse over with reduced motion consideration + */ + _handleMouseOver(e) { + if (this.disabled || !this.soundEnabled) return; + + // Only play on hover if not reduced motion + if (!globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches) { + this.state = "play"; + } + } + + /** + * Handle mouse out + */ + _handleMouseOut(e) { + if (this.disabled) return; + this.state = "pause"; + } + + /** + * Check user preferences for accessibility + */ + _checkUserPreferences() { + // Check for reduced motion preference + const prefersReducedMotion = globalThis.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + + // Check for user sound preference (could be stored in localStorage) + const userSoundPref = + globalThis.localStorage && + globalThis.localStorage.getItem("awesome-explosion-sound-enabled"); + if (userSoundPref !== null) { + this.soundEnabled = userSoundPref === "true"; + } + + // Disable sound on hover if reduced motion is preferred + if (prefersReducedMotion) { + this.soundEnabled = false; + } + } + + /** + * Get appropriate alt text for the image + */ + _getAltText() { + const colorText = this.color ? ` ${this.color}` : ""; + const sizeText = this.size !== "medium" ? ` ${this.size}` : ""; + return `${sizeText}${colorText} explosion animation`; + } + + /** + * Get appropriate ARIA label + */ + _getAriaLabel() { + const stateText = this.playing ? "playing" : "stopped"; + const soundText = this.soundEnabled ? " with sound" : " (muted)"; + return `Explosion animation ${stateText}${soundText}. Click to toggle.`; + } + /** * Set the state to play from an event. */ @@ -255,20 +443,34 @@ class AwesomeExplosion extends LitElement { } /** - * Set the state to play from an event. + * Set the state to stop from an event. */ _setStopSound(e) { this.state = "pause"; } /** - * Play the sound effect. + * Play the sound effect with accessibility considerations. */ _playSound() { + if (!this.soundEnabled || this.disabled) return; + + // Respect reduced motion preference + if (globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches) { + return; + } + if (typeof globalThis.audio === typeof undefined) { globalThis.audio = new Audio(this.sound); + // Set volume to a reasonable level + globalThis.audio.volume = 0.2; } - globalThis.audio.play(); + + // Catch and handle audio play errors gracefully + globalThis.audio.play().catch((error) => { + console.warn("Audio playback failed:", error); + // Could dispatch an event here to notify parent components + }); } } globalThis.customElements.define(AwesomeExplosion.tag, AwesomeExplosion); diff --git a/elements/awesome-explosion/custom-elements.json b/elements/awesome-explosion/custom-elements.json old mode 100755 new mode 100644 index 57cfeb2520..730c6c519d --- a/elements/awesome-explosion/custom-elements.json +++ b/elements/awesome-explosion/custom-elements.json @@ -17,6 +17,13 @@ "static": true, "readonly": true }, + { + "kind": "field", + "name": "haxProperties", + "static": true, + "description": "HAXSchema for proper integration with the HAX editor", + "readonly": true + }, { "kind": "method", "name": "_calculateStopped", @@ -61,6 +68,61 @@ "name": "_stopSound", "description": "Stop the sound effect." }, + { + "kind": "method", + "name": "_handleClick", + "parameters": [ + { + "name": "e" + } + ], + "description": "Handle click events with accessibility considerations" + }, + { + "kind": "method", + "name": "_handleKeydown", + "parameters": [ + { + "name": "e" + } + ], + "description": "Handle keyboard interactions" + }, + { + "kind": "method", + "name": "_handleMouseOver", + "parameters": [ + { + "name": "e" + } + ], + "description": "Handle mouse over with reduced motion consideration" + }, + { + "kind": "method", + "name": "_handleMouseOut", + "parameters": [ + { + "name": "e" + } + ], + "description": "Handle mouse out" + }, + { + "kind": "method", + "name": "_checkUserPreferences", + "description": "Check user preferences for accessibility" + }, + { + "kind": "method", + "name": "_getAltText", + "description": "Get appropriate alt text for the image" + }, + { + "kind": "method", + "name": "_getAriaLabel", + "description": "Get appropriate ARIA label" + }, { "kind": "method", "name": "_setPlaySound", @@ -79,12 +141,12 @@ "name": "e" } ], - "description": "Set the state to play from an event." + "description": "Set the state to stop from an event." }, { "kind": "method", "name": "_playSound", - "description": "Play the sound effect." + "description": "Play the sound effect with accessibility considerations." }, { "kind": "field", @@ -137,7 +199,7 @@ "type": { "text": "string" }, - "description": "This is to change the color of the element. Possible values are:\nred, blue, orange, yellow", + "description": "This is to change the color of the element. Possible values are:\nred, blue, orange, yellow, purple", "default": "\"\"", "attribute": "color", "reflects": true @@ -154,6 +216,38 @@ "attribute": "reset-sound", "reflects": true }, + { + "kind": "field", + "name": "soundEnabled", + "privacy": "public", + "type": { + "text": "boolean" + }, + "description": "Enable/disable sound effects globally", + "default": "true", + "attribute": "sound-enabled", + "reflects": true + }, + { + "kind": "field", + "name": "disabled", + "privacy": "public", + "type": { + "text": "boolean" + }, + "description": "Disable the entire component", + "default": "false", + "attribute": "disabled", + "reflects": true + }, + { + "kind": "field", + "name": "tabIndex", + "type": { + "text": "number" + }, + "default": "0" + }, { "kind": "field", "name": "stopped", @@ -257,7 +351,7 @@ "type": { "text": "string" }, - "description": "This is to change the color of the element. Possible values are:\nred, blue, orange, yellow", + "description": "This is to change the color of the element. Possible values are:\nred, blue, orange, yellow, purple", "default": "\"\"", "fieldName": "color" }, @@ -269,6 +363,30 @@ "description": "Allow for resetting the sound effect.", "default": "false", "fieldName": "resetSound" + }, + { + "name": "sound-enabled", + "type": { + "text": "boolean" + }, + "description": "Enable/disable sound effects globally", + "default": "true", + "fieldName": "soundEnabled" + }, + { + "name": "disabled", + "type": { + "text": "boolean" + }, + "description": "Disable the entire component", + "default": "false", + "fieldName": "disabled" + } + ], + "mixins": [ + { + "name": "DDDSuper", + "package": "@haxtheweb/d-d-d/d-d-d.js" } ], "superclass": { diff --git a/elements/awesome-explosion/demo/index.html b/elements/awesome-explosion/demo/index.html index 675bb5393b..a188257342 100644 --- a/elements/awesome-explosion/demo/index.html +++ b/elements/awesome-explosion/demo/index.html @@ -7,24 +7,93 @@ - +
-

A bunch of awesome-explosion tags

+

Awesome Explosion - DDD Design System & Accessibility Enhanced

+ + + + + + + + + + +
+

Accessibility Notes:

+
    +
  • ✅ Uses DDD design system variables for consistent sizing
  • +
  • ✅ Respects prefers-reduced-motion settings
  • +
  • ✅ Keyboard accessible (Tab + Enter/Space)
  • +
  • ✅ Screen reader friendly with proper ARIA labels
  • +
  • ✅ Sound can be controlled globally via attributes
  • +
  • ✅ Dark mode compatible with DDD theming
  • +
  • ✅ Focus indicators follow DDD focus system
  • +
+
diff --git a/elements/awesome-explosion/index.html b/elements/awesome-explosion/index.html index dfcb41285c..da8fbff977 100755 --- a/elements/awesome-explosion/index.html +++ b/elements/awesome-explosion/index.html @@ -4,10 +4,10 @@ awesome-explosion documentation - - + + - + diff --git a/elements/awesome-explosion/lib/awesome-explosion.haxProperties.json b/elements/awesome-explosion/lib/awesome-explosion.haxProperties.json new file mode 100644 index 0000000000..136ef9d579 --- /dev/null +++ b/elements/awesome-explosion/lib/awesome-explosion.haxProperties.json @@ -0,0 +1,116 @@ +{ + "api": "1", + "type": "element", + "editingElement": "core", + "canScale": false, + "canPosition": true, + "canEditSource": false, + "gizmo": { + "title": "Awesome Explosion", + "description": "An interactive explosion animation with optional sound effects", + "icon": "av:fiber-manual-record", + "color": "orange", + "tags": ["Silly", "Animation", "Interactive", "Sound", "Effect"], + "handles": [], + "meta": { + "author": "HAX The Web", + "inlineOnly": false, + "requiresChildren": false + } + }, + "settings": { + "configure": [ + { + "property": "size", + "title": "Size", + "description": "Control the size of the explosion", + "inputMethod": "select", + "options": { + "tiny": "Tiny", + "small": "Small", + "medium": "Medium", + "large": "Large", + "epic": "Epic" + } + }, + { + "property": "color", + "title": "Color", + "description": "Color theme for the explosion", + "inputMethod": "select", + "options": { + "": "Default", + "red": "Red", + "blue": "Blue", + "purple": "Purple", + "orange": "Orange", + "yellow": "Yellow" + } + }, + { + "property": "soundEnabled", + "title": "Sound Effects", + "description": "Enable or disable sound effects", + "inputMethod": "boolean" + }, + { + "property": "resetSound", + "title": "Reset Sound", + "description": "Reset sound to beginning each time", + "inputMethod": "boolean" + }, + { + "property": "image", + "title": "Custom Image", + "description": "Optional custom explosion image URL", + "inputMethod": "haxupload", + "validationType": "url" + }, + { + "property": "sound", + "title": "Custom Sound", + "description": "Optional custom sound effect URL", + "inputMethod": "haxupload", + "validationType": "url" + } + ], + "advanced": [ + { + "property": "disabled", + "title": "Disabled", + "description": "Disable all interactions", + "inputMethod": "boolean" + } + ] + }, + "demoSchema": [ + { + "tag": "awesome-explosion", + "properties": { + "size": "medium", + "color": "orange" + }, + "content": "", + "description": "A medium orange explosion" + }, + { + "tag": "awesome-explosion", + "properties": { + "size": "large", + "color": "blue", + "soundEnabled": false + }, + "content": "", + "description": "A large blue explosion without sound" + }, + { + "tag": "awesome-explosion", + "properties": { + "size": "epic", + "color": "red" + }, + "content": "", + "description": "An epic red explosion with sound" + } + ] +} diff --git a/elements/awesome-explosion/package.json b/elements/awesome-explosion/package.json old mode 100755 new mode 100644 index 53e8fb4bdc..92205afc99 --- a/elements/awesome-explosion/package.json +++ b/elements/awesome-explosion/package.json @@ -37,16 +37,13 @@ }, "license": "Apache-2.0", "dependencies": { - "lit": "3.3.0" + "lit": "3.3.1" }, "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", "@open-wc/testing": "4.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "9.1.2", "wct-browser-legacy": "1.0.2" }, diff --git a/elements/awesome-explosion/test/awesome-explosion.test.js b/elements/awesome-explosion/test/awesome-explosion.test.js index 77b278e8aa..e5c3c97bb8 100644 --- a/elements/awesome-explosion/test/awesome-explosion.test.js +++ b/elements/awesome-explosion/test/awesome-explosion.test.js @@ -4,26 +4,26 @@ import "../awesome-explosion.js"; describe("awesome-explosion test", () => { let element; let originalAudio; - + beforeEach(async () => { // Mock global audio to prevent actual sound playback during tests originalAudio = globalThis.audio; globalThis.audio = { play: () => Promise.resolve(), pause: () => {}, - currentTime: 0 + currentTime: 0, }; - + element = await fixture(html` - `); await element.updateComplete; }); - + afterEach(() => { // Restore original global audio globalThis.audio = originalAudio; @@ -51,10 +51,10 @@ describe("awesome-explosion test", () => { }); it("should have default image and sound URLs", () => { - expect(element.image).to.be.a('string'); - expect(element.image).to.include('explode.gif'); - expect(element.sound).to.be.a('string'); - expect(element.sound).to.include('.mp3'); + expect(element.image).to.be.a("string"); + expect(element.image).to.include("explode.gif"); + expect(element.sound).to.be.a("string"); + expect(element.sound).to.include(".mp3"); }); }); @@ -70,14 +70,14 @@ describe("awesome-explosion test", () => { describe("Size property", () => { it("should handle all valid size values and maintain accessibility", async () => { - const validSizes = ['tiny', 'small', 'medium', 'large', 'epic']; - + const validSizes = ["tiny", "small", "medium", "large", "epic"]; + for (const size of validSizes) { testElement.size = size; await testElement.updateComplete; expect(testElement.size).to.equal(size); - expect(testElement.hasAttribute('size')).to.be.true; - expect(testElement.getAttribute('size')).to.equal(size); + expect(testElement.hasAttribute("size")).to.be.true; + expect(testElement.getAttribute("size")).to.equal(size); await expect(testElement).shadowDom.to.be.accessible(); } }); @@ -89,15 +89,15 @@ describe("awesome-explosion test", () => { describe("Color property", () => { it("should handle all valid color values and maintain accessibility", async () => { - const validColors = ['red', 'purple', 'blue', 'orange', 'yellow', '']; - + const validColors = ["red", "purple", "blue", "orange", "yellow", ""]; + for (const color of validColors) { testElement.color = color; await testElement.updateComplete; expect(testElement.color).to.equal(color); if (color) { - expect(testElement.hasAttribute('color')).to.be.true; - expect(testElement.getAttribute('color')).to.equal(color); + expect(testElement.hasAttribute("color")).to.be.true; + expect(testElement.getAttribute("color")).to.equal(color); } await expect(testElement).shadowDom.to.be.accessible(); } @@ -110,14 +110,14 @@ describe("awesome-explosion test", () => { describe("State property", () => { it("should handle all valid state values and maintain accessibility", async () => { - const validStates = ['play', 'pause', 'stop']; - + const validStates = ["play", "pause", "stop"]; + for (const state of validStates) { testElement.state = state; await testElement.updateComplete; expect(testElement.state).to.equal(state); - expect(testElement.hasAttribute('state')).to.be.true; - expect(testElement.getAttribute('state')).to.equal(state); + expect(testElement.hasAttribute("state")).to.be.true; + expect(testElement.getAttribute("state")).to.equal(state); await expect(testElement).shadowDom.to.be.accessible(); } }); @@ -132,33 +132,33 @@ describe("awesome-explosion test", () => { testElement.resetSound = true; await testElement.updateComplete; expect(testElement.resetSound).to.equal(true); - expect(testElement.hasAttribute('reset-sound')).to.be.true; + expect(testElement.hasAttribute("reset-sound")).to.be.true; await expect(testElement).shadowDom.to.be.accessible(); testElement.resetSound = false; await testElement.updateComplete; expect(testElement.resetSound).to.equal(false); - expect(testElement.hasAttribute('reset-sound')).to.be.false; + expect(testElement.hasAttribute("reset-sound")).to.be.false; await expect(testElement).shadowDom.to.be.accessible(); }); it("should handle computed boolean properties", async () => { // Test stopped state - testElement.state = 'stop'; + testElement.state = "stop"; await testElement.updateComplete; expect(testElement.stopped).to.be.true; expect(testElement.playing).to.be.false; expect(testElement.paused).to.be.false; // Test playing state - testElement.state = 'play'; + testElement.state = "play"; await testElement.updateComplete; expect(testElement.stopped).to.be.false; expect(testElement.playing).to.be.true; expect(testElement.paused).to.be.false; // Test paused state - testElement.state = 'pause'; + testElement.state = "pause"; await testElement.updateComplete; expect(testElement.stopped).to.be.false; expect(testElement.playing).to.be.false; @@ -172,9 +172,11 @@ describe("awesome-explosion test", () => { it("should handle custom image property", async () => { testElement.image = "https://example.com/custom-explosion.gif"; await testElement.updateComplete; - expect(testElement.image).to.equal("https://example.com/custom-explosion.gif"); - - const img = testElement.shadowRoot.querySelector('#image'); + expect(testElement.image).to.equal( + "https://example.com/custom-explosion.gif", + ); + + const img = testElement.shadowRoot.querySelector("#image"); expect(img.src).to.equal("https://example.com/custom-explosion.gif"); await expect(testElement).shadowDom.to.be.accessible(); }); @@ -182,7 +184,9 @@ describe("awesome-explosion test", () => { it("should handle custom sound property", async () => { testElement.sound = "https://example.com/custom-explosion.mp3"; await testElement.updateComplete; - expect(testElement.sound).to.equal("https://example.com/custom-explosion.mp3"); + expect(testElement.sound).to.equal( + "https://example.com/custom-explosion.mp3", + ); await expect(testElement).shadowDom.to.be.accessible(); }); }); @@ -190,48 +194,50 @@ describe("awesome-explosion test", () => { describe("Visual rendering and image display", () => { it("should render image element with correct attributes", () => { - const img = element.shadowRoot.querySelector('#image'); + const img = element.shadowRoot.querySelector("#image"); expect(img).to.exist; - expect(img.tagName.toLowerCase()).to.equal('img'); - expect(img.getAttribute('loading')).to.equal('lazy'); - expect(img.getAttribute('alt')).to.equal(''); - expect(img.classList.contains('image-tag')).to.be.true; + expect(img.tagName.toLowerCase()).to.equal("img"); + expect(img.getAttribute("loading")).to.equal("lazy"); + expect(img.getAttribute("alt")).to.equal(""); + expect(img.classList.contains("image-tag")).to.be.true; }); it("should update image source when image property changes", async () => { const testElement = await fixture(html` - + `); await testElement.updateComplete; - - const img = testElement.shadowRoot.querySelector('#image'); + + const img = testElement.shadowRoot.querySelector("#image"); expect(img.src).to.equal("https://example.com/test.gif"); }); it("should apply size-based CSS classes correctly", async () => { - const sizes = ['tiny', 'small', 'medium', 'large', 'epic']; - + const sizes = ["tiny", "small", "medium", "large", "epic"]; + for (const size of sizes) { const testElement = await fixture(html` `); await testElement.updateComplete; - - expect(testElement.getAttribute('size')).to.equal(size); + + expect(testElement.getAttribute("size")).to.equal(size); await expect(testElement).shadowDom.to.be.accessible(); } }); it("should apply color-based CSS filters correctly", async () => { - const colors = ['red', 'purple', 'blue', 'orange', 'yellow']; - + const colors = ["red", "purple", "blue", "orange", "yellow"]; + for (const color of colors) { const testElement = await fixture(html` `); await testElement.updateComplete; - - expect(testElement.getAttribute('color')).to.equal(color); + + expect(testElement.getAttribute("color")).to.equal(color); await expect(testElement).shadowDom.to.be.accessible(); } }); @@ -239,11 +245,11 @@ describe("awesome-explosion test", () => { describe("Sound functionality and audio controls", () => { let mockAudio; - + beforeEach(() => { let playCallCount = 0; let pauseCallCount = 0; - + mockAudio = { play: () => { playCallCount++; @@ -254,9 +260,9 @@ describe("awesome-explosion test", () => { }, currentTime: 0, getPlayCallCount: () => playCallCount, - getPauseCallCount: () => pauseCallCount + getPauseCallCount: () => pauseCallCount, }; - + globalThis.audio = mockAudio; }); @@ -264,10 +270,10 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - - testElement.state = 'play'; + + testElement.state = "play"; await testElement.updateComplete; - + expect(mockAudio.getPlayCallCount()).to.be.greaterThan(0); expect(testElement.playing).to.be.true; }); @@ -276,10 +282,10 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - - testElement.state = 'pause'; + + testElement.state = "pause"; await testElement.updateComplete; - + expect(mockAudio.getPauseCallCount()).to.be.greaterThan(0); expect(testElement.paused).to.be.true; }); @@ -288,10 +294,10 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - - testElement.state = 'stop'; + + testElement.state = "stop"; await testElement.updateComplete; - + expect(mockAudio.getPauseCallCount()).to.be.greaterThan(0); expect(testElement.stopped).to.be.true; expect(mockAudio.currentTime).to.equal(0); @@ -301,85 +307,85 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - + mockAudio.currentTime = 5.5; // Set some progress - testElement.state = 'pause'; + testElement.state = "pause"; await testElement.updateComplete; - + expect(mockAudio.currentTime).to.equal(0); }); it("should create new Audio instance if not exists", async () => { delete globalThis.audio; - + const testElement = await fixture(html` `); - + // Mock the Audio constructor const originalAudio = globalThis.Audio; let audioCreated = false; - globalThis.Audio = function(src) { + globalThis.Audio = function (src) { audioCreated = true; this.play = () => Promise.resolve(); this.pause = () => {}; this.currentTime = 0; return this; }; - - testElement.state = 'play'; + + testElement.state = "play"; await testElement.updateComplete; - + expect(audioCreated).to.be.true; - + globalThis.Audio = originalAudio; }); }); describe("Event handling and user interaction", () => { it("should respond to click events", (done) => { - const testElement = new (element.constructor)(); - + const testElement = new element.constructor(); + // Wait for event listeners to be attached setTimeout(() => { - expect(testElement.state).to.equal('stop'); - + expect(testElement.state).to.equal("stop"); + testElement.click(); - + setTimeout(() => { - expect(testElement.state).to.equal('play'); + expect(testElement.state).to.equal("play"); done(); }, 10); }, 10); }); it("should respond to mouseover events", (done) => { - const testElement = new (element.constructor)(); - + const testElement = new element.constructor(); + setTimeout(() => { - expect(testElement.state).to.equal('stop'); - - const mouseoverEvent = new MouseEvent('mouseover'); + expect(testElement.state).to.equal("stop"); + + const mouseoverEvent = new MouseEvent("mouseover"); testElement.dispatchEvent(mouseoverEvent); - + setTimeout(() => { - expect(testElement.state).to.equal('play'); + expect(testElement.state).to.equal("play"); done(); }, 10); }, 10); }); it("should respond to mouseout events", (done) => { - const testElement = new (element.constructor)(); - + const testElement = new element.constructor(); + setTimeout(() => { - testElement.state = 'play'; - - const mouseoutEvent = new MouseEvent('mouseout'); + testElement.state = "play"; + + const mouseoutEvent = new MouseEvent("mouseout"); testElement.dispatchEvent(mouseoutEvent); - + setTimeout(() => { - expect(testElement.state).to.equal('pause'); + expect(testElement.state).to.equal("pause"); done(); }, 10); }, 10); @@ -388,46 +394,46 @@ describe("awesome-explosion test", () => { describe("Custom events and communication", () => { it("should dispatch awesome-event when playing", (done) => { - const testElement = new (element.constructor)(); - - testElement.addEventListener('awesome-event', (e) => { - expect(e.detail.message).to.equal('Sound played'); + const testElement = new element.constructor(); + + testElement.addEventListener("awesome-event", (e) => { + expect(e.detail.message).to.equal("Sound played"); expect(e.bubbles).to.be.true; expect(e.cancelable).to.be.true; expect(e.composed).to.be.true; done(); }); - - testElement.state = 'play'; + + testElement.state = "play"; }); it("should dispatch awesome-event when paused", (done) => { - const testElement = new (element.constructor)(); - - testElement.addEventListener('awesome-event', (e) => { - expect(e.detail.message).to.equal('Sound paused'); + const testElement = new element.constructor(); + + testElement.addEventListener("awesome-event", (e) => { + expect(e.detail.message).to.equal("Sound paused"); done(); }); - - testElement.state = 'pause'; + + testElement.state = "pause"; }); it("should dispatch awesome-event when stopped", (done) => { - const testElement = new (element.constructor)(); - - testElement.addEventListener('awesome-event', (e) => { - expect(e.detail.message).to.equal('Sound stopped'); + const testElement = new element.constructor(); + + testElement.addEventListener("awesome-event", (e) => { + expect(e.detail.message).to.equal("Sound stopped"); done(); }); - - testElement.state = 'stop'; + + testElement.state = "stop"; }); }); describe("Accessibility scenarios", () => { it("should remain accessible with different sizes", async () => { - const sizes = ['tiny', 'small', 'medium', 'large', 'epic']; - + const sizes = ["tiny", "small", "medium", "large", "epic"]; + for (const size of sizes) { const testElement = await fixture(html` @@ -438,8 +444,8 @@ describe("awesome-explosion test", () => { }); it("should remain accessible with different colors", async () => { - const colors = ['red', 'purple', 'blue', 'orange', 'yellow']; - + const colors = ["red", "purple", "blue", "orange", "yellow"]; + for (const color of colors) { const testElement = await fixture(html` @@ -453,8 +459,8 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - - const states = ['play', 'pause', 'stop']; + + const states = ["play", "pause", "stop"]; for (const state of states) { testElement.state = state; await testElement.updateComplete; @@ -479,9 +485,9 @@ describe("awesome-explosion test", () => { describe("Edge cases and error handling", () => { it("should handle missing global audio gracefully", () => { delete globalThis.audio; - - const testElement = new (element.constructor)(); - + + const testElement = new element.constructor(); + expect(() => { testElement._stopSound(); }).to.not.throw(); @@ -491,11 +497,11 @@ describe("awesome-explosion test", () => { const testElement = await fixture(html` `); - - testElement.state = 'invalid-state'; + + testElement.state = "invalid-state"; await testElement.updateComplete; - - expect(testElement.state).to.equal('invalid-state'); + + expect(testElement.state).to.equal("invalid-state"); expect(testElement.stopped).to.be.false; expect(testElement.playing).to.be.false; expect(testElement.paused).to.be.false; @@ -504,12 +510,15 @@ describe("awesome-explosion test", () => { it("should handle invalid size and color values", async () => { const testElement = await fixture(html` - + `); await testElement.updateComplete; - - expect(testElement.size).to.equal('invalid-size'); - expect(testElement.color).to.equal('invalid-color'); + + expect(testElement.size).to.equal("invalid-size"); + expect(testElement.color).to.equal("invalid-color"); await expect(testElement).shadowDom.to.be.accessible(); }); @@ -521,30 +530,32 @@ describe("awesome-explosion test", () => { > `); await testElement.updateComplete; - - expect(testElement.image).to.equal('invalid-url'); - expect(testElement.sound).to.equal('malformed-url'); + + expect(testElement.image).to.equal("invalid-url"); + expect(testElement.sound).to.equal("malformed-url"); await expect(testElement).shadowDom.to.be.accessible(); }); it("should handle unusual property values", async () => { - const testElement = await fixture(html``); - + const testElement = await fixture( + html``, + ); + const edgeCaseValues = [ " \t\n ", // whitespace "💥 explosion with emoji 💥", // emoji "Very long size name that might cause issues", "Multi\nline\nvalue", // multiline - "Value with 'quotes' and \"double quotes\" and special chars: !@#$%^&*()" + "Value with 'quotes' and \"double quotes\" and special chars: !@#$%^&*()", ]; - + for (const value of edgeCaseValues) { testElement.size = value; testElement.color = value; testElement.image = value; testElement.sound = value; await testElement.updateComplete; - + expect(testElement.size).to.equal(value); expect(testElement.color).to.equal(value); expect(testElement.image).to.equal(value); @@ -559,17 +570,17 @@ describe("awesome-explosion test", () => { const styles = element.constructor.styles; expect(styles).to.exist; expect(styles.length).to.be.greaterThan(0); - + const styleString = styles[0].cssText || styles[0].toString(); - expect(styleString).to.include(':host'); - expect(styleString).to.include('display: inline-block'); - expect(styleString).to.include('#image'); + expect(styleString).to.include(":host"); + expect(styleString).to.include("display: inline-block"); + expect(styleString).to.include("#image"); }); it("should include size-specific CSS rules", () => { const styles = element.constructor.styles; const styleString = styles[0].cssText || styles[0].toString(); - + expect(styleString).to.include(':host([size="tiny"])'); expect(styleString).to.include(':host([size="small"])'); expect(styleString).to.include(':host([size="medium"])'); @@ -580,39 +591,39 @@ describe("awesome-explosion test", () => { it("should include color filter CSS rules", () => { const styles = element.constructor.styles; const styleString = styles[0].cssText || styles[0].toString(); - + expect(styleString).to.include(':host([color="red"])'); expect(styleString).to.include(':host([color="purple"])'); expect(styleString).to.include(':host([color="blue"])'); expect(styleString).to.include(':host([color="orange"])'); expect(styleString).to.include(':host([color="yellow"])'); - expect(styleString).to.include('filter: sepia()'); + expect(styleString).to.include("filter: sepia()"); }); }); describe("Lifecycle and initialization", () => { it("should set up event listeners after timeout", (done) => { - const testElement = new (element.constructor)(); - + const testElement = new element.constructor(); + // Initially the event listeners shouldn't be set up yet - expect(testElement.state).to.equal('stop'); - + expect(testElement.state).to.equal("stop"); + // After timeout, event listeners should work setTimeout(() => { testElement.click(); setTimeout(() => { - expect(testElement.state).to.equal('play'); + expect(testElement.state).to.equal("play"); done(); }, 10); }, 10); }); it("should initialize with correct default media URLs", () => { - const testElement = new (element.constructor)(); - - expect(testElement.image).to.include('explode.gif'); - expect(testElement.sound).to.include('.mp3'); - expect(testElement.sound).to.include('fireworks'); + const testElement = new element.constructor(); + + expect(testElement.image).to.include("explode.gif"); + expect(testElement.sound).to.include(".mp3"); + expect(testElement.sound).to.include("fireworks"); }); }); }); diff --git a/elements/b-r/.gitignore b/elements/b-r/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/b-r/.gitignore +++ b/elements/b-r/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/b-r/demo/index.html b/elements/b-r/demo/index.html index 5c3c609c94..1a97b3b333 100644 --- a/elements/b-r/demo/index.html +++ b/elements/b-r/demo/index.html @@ -4,16 +4,15 @@ BR: b-r Demo - - +
diff --git a/elements/b-r/index.html b/elements/b-r/index.html index 831e26caab..c7737baf20 100644 --- a/elements/b-r/index.html +++ b/elements/b-r/index.html @@ -4,10 +4,10 @@ b-r documentation - - + + - + diff --git a/elements/b-r/package.json b/elements/b-r/package.json index 5049257c89..92b2da9bac 100644 --- a/elements/b-r/package.json +++ b/elements/b-r/package.json @@ -44,15 +44,12 @@ }, "license": "Apache-2.0", "dependencies": { - "lit": "3.3.0" + "lit": "3.3.1" }, "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "9.1.2", "wct-browser-legacy": "1.0.2" }, diff --git a/elements/barcode-reader/.gitignore b/elements/barcode-reader/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/barcode-reader/.gitignore +++ b/elements/barcode-reader/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/barcode-reader/demo/index.html b/elements/barcode-reader/demo/index.html index c286125546..6bffadc868 100644 --- a/elements/barcode-reader/demo/index.html +++ b/elements/barcode-reader/demo/index.html @@ -4,15 +4,14 @@ BarcodeReader: barcode-reader Demo - - +
diff --git a/elements/barcode-reader/index.html b/elements/barcode-reader/index.html index 08a49ac145..c7c828eb5a 100644 --- a/elements/barcode-reader/index.html +++ b/elements/barcode-reader/index.html @@ -4,10 +4,10 @@ barcode-reader documentation - - + + - + diff --git a/elements/barcode-reader/package.json b/elements/barcode-reader/package.json index ec88ac2cb1..9831e6f6a4 100644 --- a/elements/barcode-reader/package.json +++ b/elements/barcode-reader/package.json @@ -50,16 +50,13 @@ "@zxing/library": "0.19.1", "javascript-barcode-reader": "0.6.9", "jquery": "3.6.0", - "lit": "3.3.0" + "lit": "3.3.1" }, "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", "@open-wc/testing": "4.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "9.1.2", "gulp-babel": "8.0.0", "wct-browser-legacy": "1.0.2" diff --git a/elements/barcode-reader/test/barcode-reader.test.js b/elements/barcode-reader/test/barcode-reader.test.js index d21a602d33..166dcefaa3 100644 --- a/elements/barcode-reader/test/barcode-reader.test.js +++ b/elements/barcode-reader/test/barcode-reader.test.js @@ -4,50 +4,54 @@ import "../barcode-reader.js"; // Mock the ZXing library and related globals beforeEach(() => { // Mock ZXing library - globalThis.ZXing = function() { + globalThis.ZXing = function () { return { Runtime: { - addFunction: (callback) => () => callback(0, 10, 0, 1) + addFunction: (callback) => () => callback(0, 10, 0, 1), }, HEAPU8: { - buffer: new ArrayBuffer(1000) + buffer: new ArrayBuffer(1000), }, _resize: (width, height) => 0, - _decode_any: (ptr) => 0 + _decode_any: (ptr) => 0, }; }; - + // Mock navigator.mediaDevices globalThis.navigator = globalThis.navigator || {}; globalThis.navigator.mediaDevices = { - enumerateDevices: () => Promise.resolve([ - { deviceId: 'camera1', kind: 'videoinput', label: 'Test Camera 1' }, - { deviceId: 'camera2', kind: 'videoinput', label: 'Test Camera 2' } - ]), - getUserMedia: (constraints) => Promise.resolve({ - getTracks: () => [{ - stop: () => {} - }] - }) + enumerateDevices: () => + Promise.resolve([ + { deviceId: "camera1", kind: "videoinput", label: "Test Camera 1" }, + { deviceId: "camera2", kind: "videoinput", label: "Test Camera 2" }, + ]), + getUserMedia: (constraints) => + Promise.resolve({ + getTracks: () => [ + { + stop: () => {}, + }, + ], + }), }; - + // Mock user agent for device detection - Object.defineProperty(globalThis.navigator, 'userAgent', { - get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - configurable: true + Object.defineProperty(globalThis.navigator, "userAgent", { + get: () => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + configurable: true, }); - + // Mock ESGlobalBridge globalThis.ESGlobalBridge = { requestAvailability: () => ({ load: (name, url) => { setTimeout(() => { - globalThis.dispatchEvent(new CustomEvent('es-bridge-zxing-loaded')); + globalThis.dispatchEvent(new CustomEvent("es-bridge-zxing-loaded")); }, 10); - } - }) + }, + }), }; - + // Mock stream globalThis.stream = null; }); @@ -60,7 +64,7 @@ afterEach(() => { describe("barcode-reader test", () => { let element; - + beforeEach(async () => { element = await fixture(html``); await element.updateComplete; @@ -92,7 +96,7 @@ describe("barcode-reader test", () => { const input = element.shadowRoot.querySelector('input[type="text"]'); const button = element.shadowRoot.querySelector("simple-icon-button"); const select = element.shadowRoot.querySelector("select"); - + expect(video).to.exist; expect(canvas).to.exist; expect(input).to.exist; @@ -109,13 +113,13 @@ describe("barcode-reader test", () => { "QR_CODE_DATA", "https://example.com", "Product SKU: ABC123", - "" + "", ]; - + for (const testValue of testValues) { element.value = testValue; await element.updateComplete; - + expect(element.value).to.equal(testValue); const input = element.shadowRoot.querySelector('input[type="text"]'); expect(input.value).to.equal(testValue); @@ -126,22 +130,22 @@ describe("barcode-reader test", () => { it("should reflect value property to attribute", async () => { element.value = "test-barcode-123"; await element.updateComplete; - + expect(element.getAttribute("value")).to.equal("test-barcode-123"); }); it("should dispatch value-changed event when value changes", async () => { let eventFired = false; let eventDetail = null; - - element.addEventListener('value-changed', (e) => { + + element.addEventListener("value-changed", (e) => { eventFired = true; eventDetail = e.detail; }); - + element.value = "new-barcode-value"; await element.updateComplete; - + expect(eventFired).to.be.true; expect(eventDetail).to.equal(element); }); @@ -150,15 +154,15 @@ describe("barcode-reader test", () => { describe("scale property", () => { it("should handle numeric scale values", async () => { const scaleValues = [50, 75, 100, 125, 150]; - + for (const scale of scaleValues) { element.scale = scale; await element.updateComplete; - + expect(element.scale).to.equal(scale); const video = element.shadowRoot.querySelector("video"); - expect(video.getAttribute('width')).to.equal(`${scale}%`); - expect(video.getAttribute('height')).to.equal(`${scale}%`); + expect(video.getAttribute("width")).to.equal(`${scale}%`); + expect(video.getAttribute("height")).to.equal(`${scale}%`); await expect(element).shadowDom.to.be.accessible(); } }); @@ -166,7 +170,7 @@ describe("barcode-reader test", () => { it("should reflect scale property to attribute", async () => { element.scale = 80; await element.updateComplete; - + expect(element.getAttribute("scale")).to.equal("80"); }); }); @@ -175,15 +179,15 @@ describe("barcode-reader test", () => { it("should control input visibility", async () => { // Input should be visible by default expect(element.hideinput).to.be.false; - let inputDiv = element.shadowRoot.querySelector('.input'); - expect(inputDiv.hasAttribute('hidden')).to.be.false; - + let inputDiv = element.shadowRoot.querySelector(".input"); + expect(inputDiv.hasAttribute("hidden")).to.be.false; + // Hide input element.hideinput = true; await element.updateComplete; - - inputDiv = element.shadowRoot.querySelector('.input'); - expect(inputDiv.hasAttribute('hidden')).to.be.true; + + inputDiv = element.shadowRoot.querySelector(".input"); + expect(inputDiv.hasAttribute("hidden")).to.be.true; await expect(element).shadowDom.to.be.accessible(); }); }); @@ -201,41 +205,44 @@ describe("barcode-reader test", () => { it("should setup video and canvas elements", async () => { const video = element.shadowRoot.querySelector("video"); const canvas = element.shadowRoot.querySelector("canvas"); - - expect(video.hasAttribute('muted')).to.be.true; - expect(video.hasAttribute('autoplay')).to.be.true; - expect(video.hasAttribute('playsinline')).to.be.true; - expect(canvas.style.display).to.equal('none'); + + expect(video.hasAttribute("muted")).to.be.true; + expect(video.hasAttribute("autoplay")).to.be.true; + expect(video.hasAttribute("playsinline")).to.be.true; + expect(canvas.style.display).to.equal("none"); }); it("should handle scan button click", async () => { - await new Promise(resolve => setTimeout(resolve, 100)); // Wait for ZXing to load - - const scanButton = element.shadowRoot.querySelector('.go'); + await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for ZXing to load + + const scanButton = element.shadowRoot.querySelector(".go"); expect(scanButton).to.exist; - + // Simulate button click let clicked = false; - scanButton.onclick = () => { clicked = true; }; + scanButton.onclick = () => { + clicked = true; + }; scanButton.click(); - + expect(clicked).to.be.true; }); it("should handle camera initialization", async () => { - const renderButton = element.shadowRoot.querySelector('simple-icon-button'); + const renderButton = + element.shadowRoot.querySelector("simple-icon-button"); expect(renderButton).to.exist; - expect(renderButton.getAttribute('icon')).to.equal('image:camera-alt'); + expect(renderButton.getAttribute("icon")).to.equal("image:camera-alt"); }); }); describe("Video stream and device management", () => { it("should enumerate video devices", async () => { - await new Promise(resolve => setTimeout(resolve, 100)); - - const select = element.shadowRoot.querySelector('select'); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const select = element.shadowRoot.querySelector("select"); expect(select).to.exist; - + // The mock should populate options setTimeout(() => { expect(select.children.length).to.be.greaterThan(0); @@ -243,34 +250,37 @@ describe("barcode-reader test", () => { }); it("should handle video stream toggle", async () => { - await new Promise(resolve => setTimeout(resolve, 100)); - - const hiddenDiv = element.shadowRoot.querySelector('.hidden'); - const renderButton = element.shadowRoot.querySelector('simple-icon-button'); - - expect(hiddenDiv.style.display).to.equal(''); - + await new Promise((resolve) => setTimeout(resolve, 100)); + + const hiddenDiv = element.shadowRoot.querySelector(".hidden"); + const renderButton = + element.shadowRoot.querySelector("simple-icon-button"); + + expect(hiddenDiv.style.display).to.equal(""); + // Simulate button click to show video renderButton.click(); - await new Promise(resolve => setTimeout(resolve, 150)); - + await new Promise((resolve) => setTimeout(resolve, 150)); + // Video should be shown after click and delay }); it("should handle device detection for mobile vs PC", async () => { // Test PC detection (default mock) - expect(globalThis.navigator.userAgent).to.include('Windows'); - + expect(globalThis.navigator.userAgent).to.include("Windows"); + // Test mobile detection by changing user agent - Object.defineProperty(globalThis.navigator, 'userAgent', { - get: () => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)', - configurable: true + Object.defineProperty(globalThis.navigator, "userAgent", { + get: () => "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)", + configurable: true, }); - + // Create new element to test mobile detection - const mobileElement = await fixture(html``); + const mobileElement = await fixture( + html``, + ); await mobileElement.updateComplete; - + expect(mobileElement).to.exist; }); }); @@ -279,30 +289,30 @@ describe("barcode-reader test", () => { it("should remain accessible when input is hidden", async () => { element.hideinput = true; await element.updateComplete; - + await expect(element).shadowDom.to.be.accessible(); }); it("should remain accessible with different scale values", async () => { element.scale = 50; await element.updateComplete; - + await expect(element).shadowDom.to.be.accessible(); }); it("should maintain proper ARIA labeling", () => { - const button = element.shadowRoot.querySelector('simple-icon-button'); - const label = element.shadowRoot.querySelector('#label'); - - expect(button.getAttribute('aria-labelledby')).to.equal('label'); - expect(label.id).to.equal('label'); - expect(label.textContent).to.equal('Initialize'); + const button = element.shadowRoot.querySelector("simple-icon-button"); + const label = element.shadowRoot.querySelector("#label"); + + expect(button.getAttribute("aria-labelledby")).to.equal("label"); + expect(label.id).to.equal("label"); + expect(label.textContent).to.equal("Initialize"); }); it("should remain accessible during scanning state", async () => { element.value = "scanned-code-123"; await element.updateComplete; - + await expect(element).shadowDom.to.be.accessible(); }); }); @@ -311,47 +321,47 @@ describe("barcode-reader test", () => { it("should handle undefined scale gracefully", async () => { element.scale = undefined; await element.updateComplete; - + const video = element.shadowRoot.querySelector("video"); - expect(video.getAttribute('width')).to.equal('undefined%'); + expect(video.getAttribute("width")).to.equal("undefined%"); await expect(element).shadowDom.to.be.accessible(); }); it("should handle null value gracefully", async () => { element.value = null; await element.updateComplete; - + expect(element.value).to.be.null; const input = element.shadowRoot.querySelector('input[type="text"]'); - expect(input.value).to.equal(''); + expect(input.value).to.equal(""); await expect(element).shadowDom.to.be.accessible(); }); it("should handle very long barcode values", async () => { - const longValue = 'A'.repeat(1000); + const longValue = "A".repeat(1000); element.value = longValue; await element.updateComplete; - + expect(element.value).to.equal(longValue); await expect(element).shadowDom.to.be.accessible(); }); it("should handle special characters in barcode values", async () => { - const specialChars = '!@#$%^&*()_+-=[]{}|;\':",./<>?'; + const specialChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?"; element.value = specialChars; await element.updateComplete; - + expect(element.value).to.equal(specialChars); await expect(element).shadowDom.to.be.accessible(); }); it("should handle extreme scale values", async () => { const extremeScales = [0, -50, 1000, Number.MAX_SAFE_INTEGER]; - + for (const scale of extremeScales) { element.scale = scale; await element.updateComplete; - + expect(element.scale).to.equal(scale); await expect(element).shadowDom.to.be.accessible(); } @@ -359,10 +369,12 @@ describe("barcode-reader test", () => { it("should handle missing ZXing library gracefully", async () => { delete globalThis.ZXing; - - const testElement = await fixture(html``); + + const testElement = await fixture( + html``, + ); await testElement.updateComplete; - + // Should not throw errors even without ZXing expect(testElement).to.exist; await expect(testElement).shadowDom.to.be.accessible(); @@ -371,12 +383,14 @@ describe("barcode-reader test", () => { it("should handle media device errors gracefully", async () => { // Mock getUserMedia to reject globalThis.navigator.mediaDevices.getUserMedia = () => { - return Promise.reject(new Error('Camera not available')); + return Promise.reject(new Error("Camera not available")); }; - - const testElement = await fixture(html``); + + const testElement = await fixture( + html``, + ); await testElement.updateComplete; - + expect(testElement).to.exist; await expect(testElement).shadowDom.to.be.accessible(); }); @@ -385,9 +399,9 @@ describe("barcode-reader test", () => { describe("Lifecycle methods", () => { it("should handle firstUpdated lifecycle", async () => { // firstUpdated should be called automatically - const video = element.shadowRoot.querySelector('video'); - const canvas = element.shadowRoot.querySelector('canvas'); - + const video = element.shadowRoot.querySelector("video"); + const canvas = element.shadowRoot.querySelector("canvas"); + expect(video).to.exist; expect(canvas).to.exist; expect(element.__context).to.exist; @@ -396,94 +410,97 @@ describe("barcode-reader test", () => { it("should handle updated lifecycle with value changes", async () => { let eventFired = false; - element.addEventListener('value-changed', () => { + element.addEventListener("value-changed", () => { eventFired = true; }); - - element.value = 'updated-value'; + + element.value = "updated-value"; await element.updateComplete; - + expect(eventFired).to.be.true; }); it("should initialize properly in constructor", () => { - const newElement = new (element.constructor)(); - - expect(newElement.value).to.equal(''); + const newElement = new element.constructor(); + + expect(newElement.value).to.equal(""); expect(newElement.hideinput).to.be.false; }); }); describe("UI behavior and interaction", () => { it("should toggle video display on button click", async () => { - await new Promise(resolve => setTimeout(resolve, 100)); - - const renderButton = element.shadowRoot.querySelector('simple-icon-button'); - const hiddenDiv = element.shadowRoot.querySelector('.hidden'); - + await new Promise((resolve) => setTimeout(resolve, 100)); + + const renderButton = + element.shadowRoot.querySelector("simple-icon-button"); + const hiddenDiv = element.shadowRoot.querySelector(".hidden"); + // Initial state - expect(hiddenDiv.hasAttribute('hidden')).to.be.true; - + expect(hiddenDiv.hasAttribute("hidden")).to.be.true; + // Click to initialize and show renderButton.click(); - await new Promise(resolve => setTimeout(resolve, 150)); - + await new Promise((resolve) => setTimeout(resolve, 150)); + // Should change state after processing expect(renderButton).to.exist; }); it("should update input value when value property changes", async () => { const input = element.shadowRoot.querySelector('input[type="text"]'); - - element.value = 'test-input-sync'; + + element.value = "test-input-sync"; await element.updateComplete; - - expect(input.value).to.equal('test-input-sync'); + + expect(input.value).to.equal("test-input-sync"); }); it("should handle multiple rapid value changes", async () => { - const values = ['val1', 'val2', 'val3', 'val4', 'val5']; - + const values = ["val1", "val2", "val3", "val4", "val5"]; + for (const value of values) { element.value = value; await element.updateComplete; } - - expect(element.value).to.equal('val5'); + + expect(element.value).to.equal("val5"); const input = element.shadowRoot.querySelector('input[type="text"]'); - expect(input.value).to.equal('val5'); + expect(input.value).to.equal("val5"); }); }); describe("Performance considerations", () => { it("should handle multiple instances efficiently", async () => { const startTime = performance.now(); - + const elements = await Promise.all([ fixture(html``), fixture(html``), - fixture(html``) + fixture(html``), ]); - + const endTime = performance.now(); const creationTime = endTime - startTime; - + expect(elements.length).to.equal(3); expect(creationTime).to.be.lessThan(1000); // Should create quickly - - elements.forEach(el => { - expect(el.tagName.toLowerCase()).to.equal('barcode-reader'); + + elements.forEach((el) => { + expect(el.tagName.toLowerCase()).to.equal("barcode-reader"); }); }); it("should cleanup resources properly", async () => { - const testElement = await fixture(html``); - + const testElement = await fixture( + html``, + ); + // Simulate cleanup scenario if (globalThis.stream && globalThis.stream.getTracks) { const tracks = globalThis.stream.getTracks(); - tracks.forEach(track => { - expect(track.stop).to.be.a('function'); + tracks.forEach((track) => { + expect(track.stop).to.be.a("function"); }); } }); @@ -491,48 +508,48 @@ describe("barcode-reader test", () => { describe("CSS styles and theming", () => { it("should apply correct styles to elements", () => { - const canvas = element.shadowRoot.querySelector('canvas'); - const video = element.shadowRoot.querySelector('video'); - + const canvas = element.shadowRoot.querySelector("canvas"); + const video = element.shadowRoot.querySelector("video"); + const canvasStyles = globalThis.getComputedStyle(canvas); const videoStyles = globalThis.getComputedStyle(video); - - expect(canvasStyles.display).to.equal('none'); - expect(videoStyles.borderStyle).to.equal('solid'); + + expect(canvasStyles.display).to.equal("none"); + expect(videoStyles.borderStyle).to.equal("solid"); }); it("should handle hidden attribute styling", async () => { - element.setAttribute('hidden', ''); + element.setAttribute("hidden", ""); await element.updateComplete; - + const elementStyles = globalThis.getComputedStyle(element); - expect(elementStyles.display).to.equal('none'); + expect(elementStyles.display).to.equal("none"); }); }); describe("Custom events and integration", () => { it("should dispatch custom events with proper detail", async () => { let capturedEvent = null; - - element.addEventListener('value-changed', (e) => { + + element.addEventListener("value-changed", (e) => { capturedEvent = e; }); - - element.value = 'event-test-value'; + + element.value = "event-test-value"; await element.updateComplete; - + expect(capturedEvent).to.not.be.null; - expect(capturedEvent.type).to.equal('value-changed'); + expect(capturedEvent.type).to.equal("value-changed"); expect(capturedEvent.detail).to.equal(element); }); it("should handle ES bridge events properly", (done) => { // Test the ES bridge ZXing loaded event - globalThis.addEventListener('es-bridge-zxing-loaded', () => { + globalThis.addEventListener("es-bridge-zxing-loaded", () => { expect(true).to.be.true; // Event was fired done(); }); - + // Trigger the event (already triggered in beforeEach mock) }); }); diff --git a/elements/baseline-build-hax/.gitignore b/elements/baseline-build-hax/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/baseline-build-hax/.gitignore +++ b/elements/baseline-build-hax/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/baseline-build-hax/custom-elements.json b/elements/baseline-build-hax/custom-elements.json deleted file mode 100755 index 616ccb6acb..0000000000 --- a/elements/baseline-build-hax/custom-elements.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schemaVersion": "1.0.0", - "readme": "", - "modules": [ - { - "kind": "javascript-module", - "path": "baseline-build-hax.js", - "declarations": [], - "exports": [] - } - ] -} diff --git a/elements/baseline-build-hax/demo/index.html b/elements/baseline-build-hax/demo/index.html index 507507bafe..3f4b6a614e 100644 --- a/elements/baseline-build-hax/demo/index.html +++ b/elements/baseline-build-hax/demo/index.html @@ -9,10 +9,10 @@ - +
diff --git a/elements/baseline-build-hax/index.html b/elements/baseline-build-hax/index.html index 084d87740d..5f70e13895 100755 --- a/elements/baseline-build-hax/index.html +++ b/elements/baseline-build-hax/index.html @@ -4,10 +4,10 @@ baseline-build-hax documentation - - + + - + diff --git a/elements/baseline-build-hax/package.json b/elements/baseline-build-hax/package.json old mode 100755 new mode 100644 index 666910a8f0..a356356525 --- a/elements/baseline-build-hax/package.json +++ b/elements/baseline-build-hax/package.json @@ -74,8 +74,6 @@ "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", "@open-wc/testing": "4.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", "concurrently": "9.1.2", "wct-browser-legacy": "1.0.2" diff --git a/elements/baseline-build-hax/test/baseline-build-hax.test.js b/elements/baseline-build-hax/test/baseline-build-hax.test.js index 2cc0f007ff..ed8fdd3407 100644 --- a/elements/baseline-build-hax/test/baseline-build-hax.test.js +++ b/elements/baseline-build-hax/test/baseline-build-hax.test.js @@ -189,74 +189,74 @@ describe("baseline-build-hax module test", () => { it("should have required HAXStore methods", () => { const store = globalThis.HAXStore; - expect(store).to.have.property('requestAvailability'); - expect(typeof store.requestAvailability).to.equal('function'); + expect(store).to.have.property("requestAvailability"); + expect(typeof store.requestAvailability).to.equal("function"); }); }); describe("Component registry validation", () => { it("should have all components registered in custom elements registry", () => { const expectedComponents = [ - 'wysiwyg-hax', - 'cms-hax', - 'hax-body', - 'hax-tray', - 'hax-app-picker', - 'hax-app', - 'hax-toolbar', - 'a11y-gif-player', - 'citation-element', - 'image-compare-slider', - 'license-element', - 'lrn-math', - 'lrn-table', - 'lrn-vocab', - 'oer-schema', - 'media-image', - 'meme-maker', - 'multiple-choice', - 'person-testimonial', - 'place-holder', - 'q-r', - 'self-check', - 'stop-note', - 'video-player', - 'wikipedia-query', - 'grid-plate' + "wysiwyg-hax", + "cms-hax", + "hax-body", + "hax-tray", + "hax-app-picker", + "hax-app", + "hax-toolbar", + "a11y-gif-player", + "citation-element", + "image-compare-slider", + "license-element", + "lrn-math", + "lrn-table", + "lrn-vocab", + "oer-schema", + "media-image", + "meme-maker", + "multiple-choice", + "person-testimonial", + "place-holder", + "q-r", + "self-check", + "stop-note", + "video-player", + "wikipedia-query", + "grid-plate", ]; - expectedComponents.forEach(tagName => { + expectedComponents.forEach((tagName) => { const constructor = globalThis.customElements.get(tagName); expect(constructor).to.exist; - expect(typeof constructor).to.equal('function'); + expect(typeof constructor).to.equal("function"); }); }); it("should create instances of all registered components", () => { const componentTagNames = [ - 'wysiwyg-hax', - 'cms-hax', - 'hax-body', - 'a11y-gif-player', - 'citation-element', - 'license-element', - 'media-image', - 'meme-maker', - 'place-holder', - 'q-r', - 'self-check', - 'stop-note', - 'video-player', - 'grid-plate' + "wysiwyg-hax", + "cms-hax", + "hax-body", + "a11y-gif-player", + "citation-element", + "license-element", + "media-image", + "meme-maker", + "place-holder", + "q-r", + "self-check", + "stop-note", + "video-player", + "grid-plate", ]; - componentTagNames.forEach(tagName => { + componentTagNames.forEach((tagName) => { const element = globalThis.document.createElement(tagName); expect(element).to.exist; expect(element.tagName.toLowerCase()).to.equal(tagName); - + // Verify it's a proper custom element - expect(element.constructor.name).to.not.equal('HTMLUnknownElement'); + expect(element.constructor.name).to.not.equal("HTMLUnknownElement"); }); }); }); @@ -266,21 +266,21 @@ describe("baseline-build-hax module test", () => { // This test verifies that the import completed successfully // which means all components loaded without major issues const startTime = performance.now(); - + // Test that we can create multiple elements quickly const elements = [ - globalThis.document.createElement('hax-body'), - globalThis.document.createElement('media-image'), - globalThis.document.createElement('video-player') + globalThis.document.createElement("hax-body"), + globalThis.document.createElement("media-image"), + globalThis.document.createElement("video-player"), ]; - + const endTime = performance.now(); const creationTime = endTime - startTime; - - elements.forEach(element => { + + elements.forEach((element) => { expect(element).to.exist; }); - + // Element creation should be fast expect(creationTime).to.be.lessThan(100); }); @@ -289,24 +289,24 @@ describe("baseline-build-hax module test", () => { describe("HAX ecosystem integration", () => { it("should provide components that integrate with HAX", () => { // Test that components have HAX-related methods where expected - const haxBodyElement = globalThis.document.createElement('hax-body'); - + const haxBodyElement = globalThis.document.createElement("hax-body"); + // HAX body should have core HAX functionality expect(haxBodyElement).to.exist; - expect(haxBodyElement.tagName.toLowerCase()).to.equal('hax-body'); + expect(haxBodyElement.tagName.toLowerCase()).to.equal("hax-body"); }); it("should provide educational components", () => { // Test key educational components are available const educationalComponents = [ - 'multiple-choice', - 'self-check', - 'lrn-math', - 'lrn-vocab', - 'stop-note' + "multiple-choice", + "self-check", + "lrn-math", + "lrn-vocab", + "stop-note", ]; - educationalComponents.forEach(tagName => { + educationalComponents.forEach((tagName) => { const element = globalThis.document.createElement(tagName); expect(element).to.exist; expect(element.tagName.toLowerCase()).to.equal(tagName); @@ -316,13 +316,13 @@ describe("baseline-build-hax module test", () => { it("should provide media components", () => { // Test key media components are available const mediaComponents = [ - 'video-player', - 'media-image', - 'a11y-gif-player', - 'meme-maker' + "video-player", + "media-image", + "a11y-gif-player", + "meme-maker", ]; - mediaComponents.forEach(tagName => { + mediaComponents.forEach((tagName) => { const element = globalThis.document.createElement(tagName); expect(element).to.exist; expect(element.tagName.toLowerCase()).to.equal(tagName); @@ -333,41 +333,41 @@ describe("baseline-build-hax module test", () => { describe("Bundle completeness", () => { it("should include core HAX editing functionality", () => { const coreComponents = [ - 'hax-body', - 'hax-tray', - 'hax-toolbar', - 'hax-app-picker', - 'wysiwyg-hax', - 'cms-hax' + "hax-body", + "hax-tray", + "hax-toolbar", + "hax-app-picker", + "wysiwyg-hax", + "cms-hax", ]; - coreComponents.forEach(tagName => { + coreComponents.forEach((tagName) => { expect(globalThis.customElements.get(tagName)).to.exist; }); }); it("should include content authoring components", () => { const contentComponents = [ - 'citation-element', - 'license-element', - 'oer-schema', - 'place-holder' + "citation-element", + "license-element", + "oer-schema", + "place-holder", ]; - contentComponents.forEach(tagName => { + contentComponents.forEach((tagName) => { expect(globalThis.customElements.get(tagName)).to.exist; }); }); it("should include interactive elements", () => { const interactiveComponents = [ - 'multiple-choice', - 'self-check', - 'q-r', - 'wikipedia-query' + "multiple-choice", + "self-check", + "q-r", + "wikipedia-query", ]; - interactiveComponents.forEach(tagName => { + interactiveComponents.forEach((tagName) => { expect(globalThis.customElements.get(tagName)).to.exist; }); }); @@ -376,10 +376,10 @@ describe("baseline-build-hax module test", () => { describe("Accessibility compliance", () => { it("should include accessibility-focused components", () => { const a11yComponents = [ - 'a11y-gif-player' // Specifically named for accessibility + "a11y-gif-player", // Specifically named for accessibility ]; - a11yComponents.forEach(tagName => { + a11yComponents.forEach((tagName) => { const element = globalThis.document.createElement(tagName); expect(element).to.exist; expect(element.tagName.toLowerCase()).to.equal(tagName); @@ -388,18 +388,14 @@ describe("baseline-build-hax module test", () => { it("should create accessible elements by default", () => { // Test that key components don't have obvious accessibility issues - const componentsToTest = [ - 'hax-body', - 'media-image', - 'stop-note' - ]; + const componentsToTest = ["hax-body", "media-image", "stop-note"]; - componentsToTest.forEach(tagName => { + componentsToTest.forEach((tagName) => { const element = globalThis.document.createElement(tagName); expect(element).to.exist; - + // Elements should not have role="none" or other problematic defaults - expect(element.getAttribute('role')).to.not.equal('none'); + expect(element.getAttribute("role")).to.not.equal("none"); }); }); }); @@ -407,24 +403,20 @@ describe("baseline-build-hax module test", () => { describe("Educational content standards", () => { it("should include OER-compliant components", () => { const oerComponents = [ - 'oer-schema', - 'license-element', - 'citation-element' + "oer-schema", + "license-element", + "citation-element", ]; - oerComponents.forEach(tagName => { + oerComponents.forEach((tagName) => { expect(globalThis.customElements.get(tagName)).to.exist; }); }); it("should include learning resource components", () => { - const learningComponents = [ - 'lrn-math', - 'lrn-table', - 'lrn-vocab' - ]; + const learningComponents = ["lrn-math", "lrn-table", "lrn-vocab"]; - learningComponents.forEach(tagName => { + learningComponents.forEach((tagName) => { expect(globalThis.customElements.get(tagName)).to.exist; }); }); diff --git a/elements/beaker-broker/.gitignore b/elements/beaker-broker/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/beaker-broker/.gitignore +++ b/elements/beaker-broker/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/beaker-broker/custom-elements.json b/elements/beaker-broker/custom-elements.json old mode 100755 new mode 100644 diff --git a/elements/beaker-broker/demo/index.html b/elements/beaker-broker/demo/index.html index c37041bcc9..b2448d030e 100644 --- a/elements/beaker-broker/demo/index.html +++ b/elements/beaker-broker/demo/index.html @@ -7,10 +7,10 @@ - +
diff --git a/elements/beaker-broker/index.html b/elements/beaker-broker/index.html index b347fe91ce..f61e9963e4 100755 --- a/elements/beaker-broker/index.html +++ b/elements/beaker-broker/index.html @@ -4,10 +4,10 @@ beaker-broker documentation - - + + - + diff --git a/elements/beaker-broker/package.json b/elements/beaker-broker/package.json old mode 100755 new mode 100644 index c6bed85d19..efeb5cdda9 --- a/elements/beaker-broker/package.json +++ b/elements/beaker-broker/package.json @@ -47,10 +47,7 @@ "@custom-elements-manifest/analyzer": "0.10.4", "@haxtheweb/deduping-fix": "^11.0.0", "@open-wc/testing": "4.0.0", - "@polymer/iron-component-page": "github:PolymerElements/iron-component-page", - "@haxtheweb/utils": "^11.0.0", "@web/dev-server": "0.4.6", - "@webcomponents/webcomponentsjs": "^2.8.0", "concurrently": "9.1.2", "wct-browser-legacy": "1.0.2" }, diff --git a/elements/bootstrap-theme/.gitignore b/elements/bootstrap-theme/.gitignore index bddbc94db5..5456ae68cd 100644 --- a/elements/bootstrap-theme/.gitignore +++ b/elements/bootstrap-theme/.gitignore @@ -1,2 +1,26 @@ -node_modules -analysis-error.json \ No newline at end of file +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ +/public/ + +storybook-static +custom-elements.json +.vercel \ No newline at end of file diff --git a/elements/bootstrap-theme/bootstrap-theme.js b/elements/bootstrap-theme/bootstrap-theme.js index 439a9977f8..09e5d8e022 100644 --- a/elements/bootstrap-theme/bootstrap-theme.js +++ b/elements/bootstrap-theme/bootstrap-theme.js @@ -504,10 +504,18 @@ class BootstrapTheme extends HAXCMSThemeParts( return html`
-