diff --git a/elements/play-list/.editorconfig b/elements/play-list/.editorconfig
new file mode 100644
index 0000000000..7968a8836c
--- /dev/null
+++ b/elements/play-list/.editorconfig
@@ -0,0 +1,17 @@
+# EditorConfig: http://EditorConfig.org
+
+# Top-most EditorConfig file
+root = true
+
+# Rules for JavaScript files:
+
+[*.{js,py,json,sh,html}]
+# 4 space indentation
+indent_style = space
+indent_size = 2
+# No trailing spaces
+trim_trailing_whitespace = true
+# Unix-style newlines
+end_of_line = lf
+# Newline ending every file
+insert_final_newline = true
diff --git a/elements/play-list/.gitignore b/elements/play-list/.gitignore
new file mode 100644
index 0000000000..bddbc94db5
--- /dev/null
+++ b/elements/play-list/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+analysis-error.json
\ No newline at end of file
diff --git a/elements/play-list/.npmignore b/elements/play-list/.npmignore
new file mode 100644
index 0000000000..3c3629e647
--- /dev/null
+++ b/elements/play-list/.npmignore
@@ -0,0 +1 @@
+node_modules
diff --git a/elements/play-list/.surgeignore b/elements/play-list/.surgeignore
new file mode 100644
index 0000000000..ddf342489b
--- /dev/null
+++ b/elements/play-list/.surgeignore
@@ -0,0 +1 @@
+!node_modules/
diff --git a/elements/play-list/.travis.yml b/elements/play-list/.travis.yml
new file mode 100644
index 0000000000..0781f91604
--- /dev/null
+++ b/elements/play-list/.travis.yml
@@ -0,0 +1,18 @@
+language: node_js
+dist: trusty
+sudo: required
+addons:
+ firefox: "latest"
+ apt:
+ sources:
+ - google-chrome
+ packages:
+ - google-chrome-stable
+node_js: stable
+before_install:
+ - npm install -g web-component-tester
+install:
+ - npm install
+before_script:
+script:
+ - xvfb-run npm run test
diff --git a/elements/play-list/LICENSE.md b/elements/play-list/LICENSE.md
new file mode 100644
index 0000000000..8af104f7d7
--- /dev/null
+++ b/elements/play-list/LICENSE.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You 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,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its 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. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2023
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/elements/play-list/README.md b/elements/play-list/README.md
new file mode 100644
index 0000000000..3352fb8458
--- /dev/null
+++ b/elements/play-list/README.md
@@ -0,0 +1,63 @@
+# <play-list>
+
+List
+> scrollable component that accepts lightDom or data driven lists and generates a content player
+
+## Usage
+To use this web component in your project you can utilize one of the following styles of syntax.
+
+```js
+/* In an existing JS module / web component */
+import '@lrnwebcomponents/play-list/play-list.js';
+/* At top of an application with build routine */
+
+/* Alternatives for top of application */
+
+// via unpkg CDN (good for testing)
+
+```
+
+## Develop / Demo
+Run `yarn start` will start a local development server, open your default browser to display it, open your finder to the correct window and start watching the `/src` directory for changes and automatically rebuilding the element and documentation site for the demo.
+```bash
+yarn start
+```
+
+## Test
+
+```bash
+yarn run test
+```
+
+## Build
+Builds ensure that wcfactory can correctly compile your web component project to
+work on the maximum number of browsers possible.
+```bash
+yarn run build
+```
+
+## Contributing
+
+1. Fork it! `git clone https://github.com/elmsln/lrnwebcomponents.git`
+2. Create your feature branch: `git checkout -b my-new-feature`
+3. Commit your changes: `git commit -m 'Add some feature'`
+4. Push to the branch: `git push origin my-new-feature`
+5. Submit a pull request :D
+
+## Code style
+
+List (and all lrnwebcomponents) use [Prettier][prettier] to auto-format JS and JSON. The style rules get applied when you commit a change. If you choose to, you can [integrate your editor][prettier-ed] with Prettier to have the style rules applied on every save.
+
+[prettier]: https://github.com/prettier/prettier/
+[prettier-ed]: https://github.com/prettier/prettier/#editor-integration
+[polyserve]: https://github.com/Polymer/polyserve
+[web-component-tester]: https://github.com/Polymer/web-component-tester
+
+## License
+[ License](http://opensource.org/licenses/)
\ No newline at end of file
diff --git a/elements/play-list/custom-elements.json b/elements/play-list/custom-elements.json
new file mode 100644
index 0000000000..933f486f01
--- /dev/null
+++ b/elements/play-list/custom-elements.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "tags": [
+ {
+ "name": "play-list",
+ "description": "`play-list`\n`scrollable component that accepts lightDom or data driven lists and generates a content player`\n\n",
+ "attributes": []
+ }
+ ],
+ "globalAttributes": [],
+ "valueSets": []
+}
\ No newline at end of file
diff --git a/elements/play-list/demo/index.html b/elements/play-list/demo/index.html
new file mode 100644
index 0000000000..ab0a9118b4
--- /dev/null
+++ b/elements/play-list/demo/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ PlayList: play-list Demo
+
+
+
+
+
+
+
+
Basic play-list demo
+
+
+
+
+
+
+
+
+
diff --git a/elements/play-list/gulpfile.cjs b/elements/play-list/gulpfile.cjs
new file mode 100644
index 0000000000..3038032408
--- /dev/null
+++ b/elements/play-list/gulpfile.cjs
@@ -0,0 +1,169 @@
+const gulp = require("gulp");
+const fs = require("fs");
+const path = require("path");
+const _ = require("lodash");
+const rename = require("gulp-rename");
+const replace = require("gulp-replace");
+const stripCssComments = require("strip-css-comments");
+const decomment = require("decomment");
+const packageJson = require("./package.json");
+// merge all the src files together
+gulp.task("merge", () => {
+ return gulp
+ .src("./src/" + packageJson.wcfactory.elementName + ".js")
+ .pipe(
+ replace(
+ /\/\* REQUIRED FOR TOOLING DO NOT TOUCH \*\//g,
+ (classStatement, character, jsFile) => {
+ if (!packageJson.wcfactory.files.html) {
+ return false;
+ }
+ // pull these off the package wcfactory files area
+ let html = fs
+ .readFileSync(path.join("./", packageJson.wcfactory.files.html))
+ .toString()
+ .trim();
+ html = decomment(html);
+ let haxString = "";
+ if (packageJson.wcfactory.useHAX) {
+ let HAXProps = fs.readFileSync(
+ path.join("./", packageJson.wcfactory.files.hax)
+ );
+ haxString = `
+ // haxProperty definition
+ static get haxProperties() {
+ return ${
+ packageJson.wcfactory.sharedHaxProps &&
+ packageJson.wcfactory.sharedHaxProps.length > 0
+ ? `{
+ ...${HAXProps},
+ ${packageJson.wcfactory.sharedHaxProps.join(', ')}
+ }` : HAXProps
+ };
+ }`;
+ }
+ let rawprops = "{}";
+ rawprops = fs.readFileSync(
+ path.join("./", packageJson.wcfactory.files.properties)
+ );
+ let props = `${rawprops}`,
+ comma = props
+ .replace(/\/\*[\s\S]*?\*\//g, "")
+ .replace(/\/\/.*/g, "")
+ .replace(/[\{\s\n\}]/g, "");
+ (props = props.replace(/\"type\": \"(\w+)\"/g, '"type": $1')),
+ (superprops =
+ comma === "" ? `...super.properties` : `...super.properties,`);
+ props = props.replace(/\{([\s\n]*)/, `{$1$1${superprops}$1$1`);
+ let cssResult = "";
+ if (
+ packageJson.wcfactory.useSass &&
+ packageJson.wcfactory.files.scss
+ ) {
+ const sass = require("node-sass");
+ cssResult += sass.renderSync({
+ file: path.join("./", packageJson.wcfactory.files.scss)
+ }).css;
+ } else if (packageJson.wcfactory.files.css) {
+ cssResult += fs.readFileSync(
+ path.join("./", packageJson.wcfactory.files.css)
+ );
+ }
+
+ cssResult = stripCssComments(cssResult).trim();
+ let litResult =
+ packageJson.wcfactory.customElementClass !== "LitElement"
+ ? ``
+ : `
+ //styles function
+ static get styles() {
+ return [
+ ${
+ packageJson.wcfactory.sharedStyles &&
+ packageJson.wcfactory.sharedStyles.length > 0
+ ? `${packageJson.wcfactory.sharedStyles.join(",")},`
+ : ``
+ }
+ css\`
+${cssResult}
+ \`
+ ];
+ }`,
+ styleResult =
+ packageJson.wcfactory.customElementClass !== "LitElement"
+ ? ``
+ : ``;
+
+ return `${litResult}
+
+// Template return function
+ render() {
+ return html\`
+${styleResult}
+${html}\`;
+ }
+${haxString}
+ // properties available to the custom element for data binding
+ static get properties() {
+ return ${props};
+ }`;
+ }
+ )
+ )
+ .pipe(gulp.dest("./"));
+});
+// run polymer analyze to generate documentation
+gulp.task("analyze", () => {
+ var exec = require("child_process").exec;
+ return exec(
+ "polymer analyze --input demo/index.html > analysis.json",
+ function(error, stdout, stderr) {
+ if (error !== null) {
+ console.log("exec error: " + error);
+ }
+ }
+ );
+});
+
+gulp.task("watch", () => {
+ return gulp.watch("./src/*", gulp.series("merge", "analyze"));
+});
+
+gulp.task("dev", gulp.series("merge", "analyze", "watch"));
+
+// discover iconset and build json structure
+gulp.task("iconset", (done) => {
+ const iconset = packageJson.wcfactory.iconset || {};
+ if(iconset.svgsPath && iconset.svgsPath !== ''){
+ const path = iconset.svgsPath;
+ const manifestFilename = iconset.manifestFilename || `${packageJson.wcfactory.elementName}-iconsets-manifest.js`
+ const manifestPath = iconset.manifestPath || `./lib`;
+ const exportName = iconset.exportName || `${packageJson.wcfactory.className}IconsetsManifest`;
+ const jsonContent = JSON.stringify(dirTree(path).icons, null, 2);
+ const iconVar = `import { SimpleIconsetStore } from "@lrnwebcomponents/simple-icon/lib/simple-iconset.js";
+
+/**
+ * @const ${exportName}
+ */
+export const ${exportName} = ${jsonContent};
+SimpleIconsetStore.registerManifest(${exportName});`
+ fs.writeFile(`${manifestPath}/${manifestFilename}.js`, iconVar, 'utf8', function (err) {
+ if (err) {
+ console.log("An error occured while writing iconset manifest Object to File.");
+ return console.log(err);
+ }
+ console.log("Iconset SVGs and manifest JS file has been saved.");
+ return true;
+ });
+ } else {
+ console.log("No Iconset Manifest");
+ }
+ done();
+});
+
+gulp.task(
+ "default",
+ gulp.series("merge", "analyze")
+);
diff --git a/elements/play-list/index.html b/elements/play-list/index.html
new file mode 100644
index 0000000000..846278e634
--- /dev/null
+++ b/elements/play-list/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ play-list documentation
+
+
+
+
+
+
+
diff --git a/elements/play-list/lib/.gitkeep b/elements/play-list/lib/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/elements/play-list/lib/SLStyleManager.js b/elements/play-list/lib/SLStyleManager.js
new file mode 100644
index 0000000000..fbb8cfe80a
--- /dev/null
+++ b/elements/play-list/lib/SLStyleManager.js
@@ -0,0 +1,25 @@
+// generate the light/dark link elements so that we style things correctly when used
+export function generateStyleLinkEls() {
+ // ensure we only have the 2 we need
+ if (document.head && document.head.querySelector('#showlace-light') && document.head.querySelector('#showlace-dark')) {
+ return false;
+ }
+
+ const light = new URL("../../../node_modules/@shoelace-style/shoelace/dist/themes/light.css", import.meta.url)
+ let link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("media", "(prefers-color-scheme:light)");
+ link.setAttribute("href", light);
+ link.setAttribute("id", "showlace-light");
+ document.head.appendChild(link);
+
+ const dark = new URL("../../../node_modules/@shoelace-style/shoelace/dist/themes/dark.css", import.meta.url)
+ link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("media", "(prefers-color-scheme:dark)");
+ link.onload = () => { document.documentElement.classList.add('sl-theme-dark'); };
+ link.setAttribute("href", dark);
+ link.setAttribute("id", "showlace-dark");
+ document.head.appendChild(link);
+ return true;
+ }
\ No newline at end of file
diff --git a/elements/play-list/lib/play-list.haxProperties.json b/elements/play-list/lib/play-list.haxProperties.json
new file mode 100644
index 0000000000..5f5b068991
--- /dev/null
+++ b/elements/play-list/lib/play-list.haxProperties.json
@@ -0,0 +1,39 @@
+{
+ "api": "1",
+ "canScale": true,
+ "canPosition": true,
+ "canEditSource": true,
+ "contentEditable": false,
+ "gizmo": {
+ "title": "play list",
+ "description": "",
+ "icon": "icons:android",
+ "color": "purple",
+ "tags": [
+ "Other"
+ ],
+ "handles": [],
+ "meta": {
+ "author": "HAXTheWeb core team"
+ }
+ },
+ "settings": {
+ "configure": [],
+ "advanced": [],
+ "developer": []
+ },
+ "saveOptions": {
+ "unsetAttributes": []
+ },
+ "documentation": {
+ "howTo": null,
+ "purpose": null
+ },
+ "demoSchema": [
+ {
+ "tag": "play-list",
+ "content": "",
+ "properties": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/elements/play-list/package.json b/elements/play-list/package.json
new file mode 100644
index 0000000000..082345cde7
--- /dev/null
+++ b/elements/play-list/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "@lrnwebcomponents/play-list",
+ "wcfactory": {
+ "className": "PlayList",
+ "customElementClass": "LitElement",
+ "elementName": "play-list",
+ "generator-wcfactory-version": "0.13.1",
+ "useHAX": false,
+ "useSass": false,
+ "files": {
+ "css": "src/play-list.css",
+ "scss": "src/play-list.scss",
+ "html": "src/play-list.html",
+ "js": "src/play-list.js",
+ "properties": "src/play-list-properties.json",
+ "hax": "src/play-list-hax.json"
+ },
+ "iconset": {},
+ "sharedHaxProps": [],
+ "sharedProps": [],
+ "sharedStyles": []
+ },
+ "version": "7.0.15",
+ "description": "scrollable component that accepts lightDom or data driven lists and generates a content player",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/elmsln/lrnwebcomponents.git"
+ },
+ "main": "play-list.js",
+ "module": "play-list.js",
+ "umd": "play-list.umd.js",
+ "scripts": {
+ "test": "web-test-runner \"test/**/*.test.js\" --node-resolve --config=../../web-test-runner.config.mjs --playwright --browsers chromium firefox",
+ "test:watch": "web-test-runner \"test/**/*.test.js\" --node-resolve --watch --config=../../web-test-runner.config.mjs --playwright --browsers chromium firefox",
+ "start": "yarn run dev",
+ "build": "gulp --gulpfile=gulpfile.cjs && prettier --ignore-path ../../.prettierignore --write '**/*.{js,json}' && wca analyze \"*.js|lib/**.js\" --format vscode --outFile custom-elements.json",
+ "dev": "concurrently --kill-others \"yarn run watch\" \"yarn run serve\"",
+ "watch": "gulp dev --gulpfile=gulpfile.cjs",
+ "serve": "web-dev-server -c ../../web-dev-server.config.mjs",
+ "lighthouse": "gulp lighthouse --gulpfile=gulpfile.cjs"
+ },
+ "author": {
+ "name": ""
+ },
+ "license": "",
+ "dependencies": {
+ "@shoelace-style/shoelace": "2.6.0",
+ "lit": "^2.7.5"
+ },
+ "devDependencies": {
+ "@wcfactory/rollup-umd-build": "^0.12.1",
+ "@lrnwebcomponents/deduping-fix": "^7.0.4",
+ "@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",
+ "lodash": "4.17.21",
+ "polymer-build": "3.1.4",
+ "@web/dev-server": "0.1.28",
+ "wct-browser-legacy": "1.0.2",
+ "web-animations-js": "2.3.2",
+ "@open-wc/testing": "2.5.32"
+ },
+ "private": false,
+ "publishConfig": {
+ "access": "public"
+ },
+ "keywords": [
+ "web-components",
+ "html"
+ ]
+}
diff --git a/elements/play-list/play-list.js b/elements/play-list/play-list.js
new file mode 100644
index 0000000000..c7c873c828
--- /dev/null
+++ b/elements/play-list/play-list.js
@@ -0,0 +1,189 @@
+/**
+ * Copyright 2023
+ * @license , see License.md for full text.
+ */
+import { LitElement, html, css } from "lit";
+import "@shoelace-style/shoelace/dist/components/carousel/carousel.js";
+import "@shoelace-style/shoelace/dist/components/carousel-item/carousel-item.js";
+import "@lrnwebcomponents/video-player/video-player.js";
+import { generateStyleLinkEls } from "./lib/SLStyleManager.js";
+
+/**
+ * `play-list`
+ * `scrollable component that accepts lightDom or data driven lists and generates a content player`
+ * @demo demo/index.html
+ * @element play-list
+ */
+class PlayList extends LitElement {
+ /**
+ * HTMLElement
+ */
+ constructor() {
+ super();
+ // handles SL styles link elements
+ generateStyleLinkEls();
+ this.items = [
+ {
+ src: "https://shoelace.style/assets/examples/carousel/mountains.jpg",
+ alt: "A picture of a cat",
+ },
+ {
+ src: "https://shoelace.style/assets/examples/carousel/mountains.jpg",
+ alt: "A picture of a mountain",
+ }
+ ];
+ this.navigation = true;
+ this.pagination = true;
+ this.aspectRatio = "16:9";
+ this.slide = 0;
+ this.orientation = "horizontal";
+ // @todo add a mutation observer to watch for changes to the light dom
+ // and then use that to update the items array so that we can translate
+ // we have to do this so that we can ensure quality control of the light dom
+ }
+
+ /**
+ * haxProperties integration via file reference
+ */
+ static get haxProperties() {
+ return new URL(`./lib/${this.tag}.haxProperties.json`, import.meta.url).href;
+ }
+
+ disconnectedCallback() {
+ if (this._linkEls) {
+ document.head.removeChild(this._linkEls[0]);
+ document.head.removeChild(this._linkEls[1]);
+ }
+ super.disconnectedCallback();
+ }
+
+ static get properties() {
+ return {
+ items: { type: Array },
+ navigation: { type: Boolean, reflect: true },
+ pagination: { type: Boolean, reflect: true },
+ aspectRatio: { type: String, reflect: true, attribute: 'aspect-ratio' },
+ orientation: { type: String, reflect: true },
+ slide: { type: Number, reflect: true },
+ }
+ }
+
+ /**
+ * LitElement style callback
+ */
+ static get styles() {
+ // support for using in other classes
+ let styles = [];
+ if (super.styles) {
+ styles = super.styles;
+ }
+ return [
+ ...styles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([orientation="vertical"]),
+ :host([orientation="vertical"]) .carousel,
+ :host([orientation="vertical"]) .carousel .item {
+ max-height: 400px;
+ }
+ :host([orientation="vertical"]) .carousel .item video-player {
+ max-height: 400px;
+ width: 500px;
+ }
+
+ :host .carousel .item * {
+ width: 100%;
+ }
+
+
+ :host([orientation="vertical"]) .carousel::part(base) {
+ grid-template-areas: 'slides slides pagination';
+ }
+
+ :host([orientation="vertical"]) .carousel::part(pagination) {
+ flex-direction: column;
+ }
+
+ :host([orientation="vertical"]) .carousel::part(navigation) {
+ transform: rotate(90deg);
+ display: flex;
+ }
+
+
+ `,
+ ];
+ }
+ /**
+ * LitElement render callback
+ */
+ render() {
+ return html`
+
+ ${this.items.map((item) => html`
+
+
+
+ `)}
+
+
+
+
+ `;
+ }
+
+ slideIndexChanged(e) {
+ this.slide = e.detail.index;
+ }
+ /**
+ * Convention we use
+ */
+ static get tag() {
+ return "play-list";
+ }
+ /**
+ * LitElement ready
+ */
+ firstUpdated(changedProperties) {
+ if (super.firstUpdated) {
+ super.firstUpdated(changedProperties);
+ }
+ }
+ /**
+ * LitElement life cycle - property changed
+ */
+ updated(changedProperties) {
+ if (super.updated) {
+ super.updated(changedProperties);
+ }
+ changedProperties.forEach((oldValue, propName) => {
+ // sync slide index with changes in the carousel
+ if (propName == 'slide' && this.shadowRoot && typeof oldValue !== typeof undefined) {
+ this.dispatchEvent(
+ new CustomEvent(`${propName}-changed`, {
+ detail: {
+ value: this[propName],
+ }
+ })
+ );
+ if (this.shadowRoot.querySelector('.carousel').activeSlide !== this[propName]) {
+ this.shadowRoot.querySelector('.carousel').goToSlide(parseInt(this[propName]));
+ }
+ }
+ });
+ }
+}
+customElements.define(PlayList.tag, PlayList);
+export { PlayList };
diff --git a/elements/play-list/play-list.stories.js b/elements/play-list/play-list.stories.js
new file mode 100644
index 0000000000..bc475bab76
--- /dev/null
+++ b/elements/play-list/play-list.stories.js
@@ -0,0 +1,22 @@
+import { html } from "lit-html";
+import { withKnobs, text, boolean } from "@open-wc/demoing-storybook";
+import { StorybookUtilities } from "@lrnwebcomponents/storybook-utilities/storybook-utilities.js";
+import { PlayList } from "./play-list.js";
+// need to account for polymer goofiness when webpack rolls this up
+
+export default {
+ title: "List|PlayList",
+ component: "play-list",
+ decorators: [withKnobs],
+ parameters: {
+ options: { selectedPanel: "storybookjs/knobs/panel" },
+ },
+};
+const utils = new StorybookUtilities();
+export const PlayListStory = () => {
+ return utils.makeUsageDocs(
+ PlayList,
+ import.meta.url,
+ utils.makeElementFromClass(PlayList)
+ );
+};
diff --git a/elements/play-list/polymer.json b/elements/play-list/polymer.json
new file mode 100644
index 0000000000..72197685c3
--- /dev/null
+++ b/elements/play-list/polymer.json
@@ -0,0 +1,31 @@
+{
+ "entrypoint": "demo/index.html",
+ "extraDependencies": [
+ "node_modules/@webcomponents/webcomponentsjs/*.js",
+ "!node_modules/@webcomponents/webcomponentsjs/gulpfile.js",
+ "node_modules/@webcomponents/webcomponentsjs/bundles/*.js"
+ ],
+ "sources": [],
+ "builds": [
+ {
+ "name": "es6",
+ "browserCapabilities": ["es2018", "modules"],
+ "js": {
+ "minify": false,
+ "transformImportMeta": true
+ },
+ "css": {
+ "minify": false
+ },
+ "html": {
+ "minify": false
+ },
+ "bundle": false
+ }
+ ],
+ "moduleResolution": "node",
+ "npm": true,
+ "lint": {
+ "rules": ["polymer-3"]
+ }
+}
diff --git a/elements/play-list/src/play-list.js b/elements/play-list/src/play-list.js
new file mode 100644
index 0000000000..c7c873c828
--- /dev/null
+++ b/elements/play-list/src/play-list.js
@@ -0,0 +1,189 @@
+/**
+ * Copyright 2023
+ * @license , see License.md for full text.
+ */
+import { LitElement, html, css } from "lit";
+import "@shoelace-style/shoelace/dist/components/carousel/carousel.js";
+import "@shoelace-style/shoelace/dist/components/carousel-item/carousel-item.js";
+import "@lrnwebcomponents/video-player/video-player.js";
+import { generateStyleLinkEls } from "./lib/SLStyleManager.js";
+
+/**
+ * `play-list`
+ * `scrollable component that accepts lightDom or data driven lists and generates a content player`
+ * @demo demo/index.html
+ * @element play-list
+ */
+class PlayList extends LitElement {
+ /**
+ * HTMLElement
+ */
+ constructor() {
+ super();
+ // handles SL styles link elements
+ generateStyleLinkEls();
+ this.items = [
+ {
+ src: "https://shoelace.style/assets/examples/carousel/mountains.jpg",
+ alt: "A picture of a cat",
+ },
+ {
+ src: "https://shoelace.style/assets/examples/carousel/mountains.jpg",
+ alt: "A picture of a mountain",
+ }
+ ];
+ this.navigation = true;
+ this.pagination = true;
+ this.aspectRatio = "16:9";
+ this.slide = 0;
+ this.orientation = "horizontal";
+ // @todo add a mutation observer to watch for changes to the light dom
+ // and then use that to update the items array so that we can translate
+ // we have to do this so that we can ensure quality control of the light dom
+ }
+
+ /**
+ * haxProperties integration via file reference
+ */
+ static get haxProperties() {
+ return new URL(`./lib/${this.tag}.haxProperties.json`, import.meta.url).href;
+ }
+
+ disconnectedCallback() {
+ if (this._linkEls) {
+ document.head.removeChild(this._linkEls[0]);
+ document.head.removeChild(this._linkEls[1]);
+ }
+ super.disconnectedCallback();
+ }
+
+ static get properties() {
+ return {
+ items: { type: Array },
+ navigation: { type: Boolean, reflect: true },
+ pagination: { type: Boolean, reflect: true },
+ aspectRatio: { type: String, reflect: true, attribute: 'aspect-ratio' },
+ orientation: { type: String, reflect: true },
+ slide: { type: Number, reflect: true },
+ }
+ }
+
+ /**
+ * LitElement style callback
+ */
+ static get styles() {
+ // support for using in other classes
+ let styles = [];
+ if (super.styles) {
+ styles = super.styles;
+ }
+ return [
+ ...styles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([orientation="vertical"]),
+ :host([orientation="vertical"]) .carousel,
+ :host([orientation="vertical"]) .carousel .item {
+ max-height: 400px;
+ }
+ :host([orientation="vertical"]) .carousel .item video-player {
+ max-height: 400px;
+ width: 500px;
+ }
+
+ :host .carousel .item * {
+ width: 100%;
+ }
+
+
+ :host([orientation="vertical"]) .carousel::part(base) {
+ grid-template-areas: 'slides slides pagination';
+ }
+
+ :host([orientation="vertical"]) .carousel::part(pagination) {
+ flex-direction: column;
+ }
+
+ :host([orientation="vertical"]) .carousel::part(navigation) {
+ transform: rotate(90deg);
+ display: flex;
+ }
+
+
+ `,
+ ];
+ }
+ /**
+ * LitElement render callback
+ */
+ render() {
+ return html`
+
+ ${this.items.map((item) => html`
+
+
+
+ `)}
+
+
+
+
+ `;
+ }
+
+ slideIndexChanged(e) {
+ this.slide = e.detail.index;
+ }
+ /**
+ * Convention we use
+ */
+ static get tag() {
+ return "play-list";
+ }
+ /**
+ * LitElement ready
+ */
+ firstUpdated(changedProperties) {
+ if (super.firstUpdated) {
+ super.firstUpdated(changedProperties);
+ }
+ }
+ /**
+ * LitElement life cycle - property changed
+ */
+ updated(changedProperties) {
+ if (super.updated) {
+ super.updated(changedProperties);
+ }
+ changedProperties.forEach((oldValue, propName) => {
+ // sync slide index with changes in the carousel
+ if (propName == 'slide' && this.shadowRoot && typeof oldValue !== typeof undefined) {
+ this.dispatchEvent(
+ new CustomEvent(`${propName}-changed`, {
+ detail: {
+ value: this[propName],
+ }
+ })
+ );
+ if (this.shadowRoot.querySelector('.carousel').activeSlide !== this[propName]) {
+ this.shadowRoot.querySelector('.carousel').goToSlide(parseInt(this[propName]));
+ }
+ }
+ });
+ }
+}
+customElements.define(PlayList.tag, PlayList);
+export { PlayList };
diff --git a/elements/play-list/test/play-list.test.js b/elements/play-list/test/play-list.test.js
new file mode 100644
index 0000000000..af0b28fe4f
--- /dev/null
+++ b/elements/play-list/test/play-list.test.js
@@ -0,0 +1,17 @@
+import { fixture, expect, html } from "@open-wc/testing";
+import "../play-list.js";
+
+describe("elementName test", () => {
+ let element;
+ beforeEach(async () => {
+ element = await fixture(html``);
+ });
+
+ it("basic will it blend", async () => {
+ expect(element).to.exist;
+ });
+
+ it("passes the a11y audit", async () => {
+ await expect(element).shadowDom.to.be.accessible();
+ });
+});