Compare commits

..

8 Commits

Author SHA1 Message Date
Wendelin
efece17f50 Merge branch 'dev' of github.com:home-assistant/frontend into gulp-ts 2025-07-24 08:25:06 +02:00
Wendelin
79e5c59fdf fix duplicates 2025-05-28 15:23:59 +02:00
Wendelin
0aa34a14dd Fix lint, remove unused file 2025-05-28 15:05:06 +02:00
Wendelin
1ced9959fa Merge branch 'dev' of github.com:home-assistant/frontend into gulp-ts 2025-05-28 14:54:09 +02:00
Wendelin
1b67a6f358 Use tsx to run gulp 2025-05-28 14:50:25 +02:00
Wendelin
62f2b286ae Fix scripts 2025-05-09 13:17:25 +02:00
Wendelin
8f7760f88f Implement correct types 2025-05-09 13:07:09 +02:00
Wendelin
ff3b65605e Use TS with gulp 2025-05-09 08:23:53 +02:00
772 changed files with 13077 additions and 30842 deletions

View File

@@ -310,11 +310,7 @@ export class DialogMyFeature
.heading=${createCloseHeading(this.hass, this._params.title)} .heading=${createCloseHeading(this.hass, this._params.title)}
> >
<!-- Dialog content --> <!-- Dialog content -->
<ha-button <ha-button @click=${this.closeDialog} slot="secondaryAction">
appearance="plain"
@click=${this.closeDialog}
slot="secondaryAction"
>
${this.hass.localize("ui.common.cancel")} ${this.hass.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button @click=${this._submit} slot="primaryAction"> <ha-button @click=${this._submit} slot="primaryAction">

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -35,7 +35,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Cast - name: Build Cast
run: ./node_modules/.bin/gulp build-cast run: yarn run-task build-cast
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -56,12 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -70,7 +70,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Cast - name: Build Cast
run: ./node_modules/.bin/gulp build-cast run: yarn run-task build-cast
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -35,9 +35,9 @@ jobs:
- name: Check for duplicate dependencies - name: Check for duplicate dependencies
run: yarn dedupe --check run: yarn dedupe --check
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages run: yarn run-task gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache - name: Setup lint cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: | path: |
node_modules/.cache/prettier node_modules/.cache/prettier
@@ -58,16 +58,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data run: yarn run-task gen-icons-json build-translations build-locale-data
- name: Run Tests - name: Run Tests
run: yarn run test run: yarn run test
build: build:
@@ -76,20 +76,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Build Application - name: Build Application
run: ./node_modules/.bin/gulp build-app run: yarn run-task build-app
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@@ -100,20 +100,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Build Application - name: Build Application
run: ./node_modules/.bin/gulp build-hassio run: yarn run-task build-hassio
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 uses: github/codeql-action/analyze@v3

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -36,7 +36,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Demo - name: Build Demo
run: ./node_modules/.bin/gulp build-demo run: yarn run-task build-demo
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -71,7 +71,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Demo - name: Build Demo
run: ./node_modules/.bin/gulp build-demo run: yarn run-task build-demo
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -28,7 +28,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Gallery - name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery run: yarn run-task build-gallery
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -33,7 +33,7 @@ jobs:
run: yarn install --immutable run: yarn install --immutable
- name: Build Gallery - name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery run: yarn run-task build-gallery
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Apply labels - name: Apply labels
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 uses: actions/labeler@v5.0.0
with: with:
sync-labels: true sync-labels: true

View File

@@ -9,7 +9,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 - uses: dessant/lock-threads@v5.0.1
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
process-only: "issues, prs" process-only: "issues, prs"

View File

@@ -20,15 +20,15 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 uses: actions/setup-python@v5
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send bundle stats and build information to RelativeCI - name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1 uses: relative-ci/agent-action@v3.0.0
with: with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }} token: ${{ github.token }}

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read pull-requests: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 - uses: release-drafter/release-drafter@v6.1.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,10 +23,10 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@v5
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@v2.3.2
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -73,9 +73,8 @@ jobs:
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.07.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: cp313 abi: cp313
tag: musllinux_1_2 tag: musllinux_1_2
@@ -91,9 +90,9 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -108,7 +107,7 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@v2.3.2
with: with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -120,9 +119,9 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@v4.4.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -137,6 +136,6 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@v2.3.2
with: with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -9,10 +9,10 @@ jobs:
check-authorization: check-authorization:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only run if this is a Task issue type (from the issue form) # Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task' if: github.event.issue.issue_type == 'Task'
steps: steps:
- name: Check if user is authorized - name: Check if user is authorized
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@v7
with: with:
script: | script: |
const issueAuthor = context.payload.issue.user.login; const issueAuthor = context.payload.issue.user.login;

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 90 days stale policy - name: 90 days stale policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 uses: actions/stale@v9.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@v4.2.2
- name: Upload Translations - name: Upload Translations
run: | run: |

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.10.0.cjs yarnPath: .yarn/releases/yarn-4.9.2.cjs

View File

@@ -1,8 +0,0 @@
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Part of the frontend that mobile developper should review
src/external_app/ @bgoncal @TimoPtr
test/external_app/ @bgoncal @TimoPtr

View File

@@ -1,6 +1,6 @@
import defineProvider from "@babel/helper-define-polyfill-provider"; import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path"; import { join } from "node:path";
import paths from "../paths.cjs"; import paths from "../paths";
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills"); const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");

View File

@@ -1,42 +1,41 @@
const path = require("path"); import path from "node:path";
const env = require("./env.cjs"); import packageJson from "../package.json" assert { type: "json" };
const paths = require("./paths.cjs"); import { version } from "./env.ts";
const { dependencies } = require("../package.json"); import paths, { dirname } from "./paths.ts";
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins"); const dependencies = packageJson.dependencies;
const BABEL_PLUGINS = path.join(dirname, "babel-plugins");
// GitHub base URL to use for production source maps // GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
module.exports.sourceMapURL = () => { export const sourceMapURL = () => {
const ref = env.version().endsWith("dev") const ref = version().endsWith("dev")
? process.env.GITHUB_SHA || "dev" ? process.env.GITHUB_SHA || "dev"
: env.version(); : version();
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`; return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
}; };
// Files from NPM Packages that should not be imported
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file // Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild }) => export const emptyPackages = ({ isHassioBuild }) =>
[ [
require.resolve("@vaadin/vaadin-material-styles/typography.js"), import.meta.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"), import.meta.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load. // Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild && isHassioBuild &&
require.resolve( import.meta.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts") path.resolve(paths.root_dir, "src/components/ha-icon.ts")
), ),
isHassioBuild && isHassioBuild &&
require.resolve( import.meta.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts") path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
), ),
].filter(Boolean); ].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ export const definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__DEV__: !isProdBuild, __DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), __BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__VERSION__: JSON.stringify(env.version()), __VERSION__: JSON.stringify(version()),
__DEMO__: false, __DEMO__: false,
__SUPERVISOR__: false, __SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false, __BACKWARDS_COMPAT__: false,
@@ -53,7 +52,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
...defineOverlay, ...defineOverlay,
}); });
module.exports.htmlMinifierOptions = { export const htmlMinifierOptions = {
caseSensitive: true, caseSensitive: true,
collapseWhitespace: true, collapseWhitespace: true,
conservativeCollapse: true, conservativeCollapse: true,
@@ -65,16 +64,16 @@ module.exports.htmlMinifierOptions = {
}, },
}; };
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({ export const terserOptions = ({ latestBuild, isTestBuild }) => ({
safari10: !latestBuild, safari10: !latestBuild,
ecma: latestBuild ? 2015 : 5, ecma: latestBuild ? (2015 as const) : (5 as const),
module: latestBuild, module: latestBuild,
format: { comments: false }, format: { comments: false },
sourceMap: !isTestBuild, sourceMap: !isTestBuild,
}); });
/** @type {import('@rspack/core').SwcLoaderOptions} */ /** @type {import('@rspack/core').SwcLoaderOptions} */
module.exports.swcOptions = () => ({ export const swcOptions = () => ({
jsc: { jsc: {
loose: true, loose: true,
externalHelpers: true, externalHelpers: true,
@@ -86,11 +85,16 @@ module.exports.swcOptions = () => ({
}, },
}); });
module.exports.babelOptions = ({ export const babelOptions = ({
latestBuild, latestBuild,
isProdBuild, isProdBuild,
isTestBuild, isTestBuild,
sw, sw,
}: {
latestBuild?: boolean;
isProdBuild?: boolean;
isTestBuild?: boolean;
sw?: boolean;
}) => ({ }) => ({
babelrc: false, babelrc: false,
compact: false, compact: false,
@@ -137,7 +141,7 @@ module.exports.babelOptions = ({
"@polymer/polymer/lib/utils/html-tag.js": ["html"], "@polymer/polymer/lib/utils/html-tag.js": ["html"],
}, },
strictCSS: true, strictCSS: true,
htmlMinifier: module.exports.htmlMinifierOptions, htmlMinifier: htmlMinifierOptions,
failOnError: false, // we can turn this off in case of false positives failOnError: false, // we can turn this off in case of false positives
}, },
], ],
@@ -160,7 +164,7 @@ module.exports.babelOptions = ({
// themselves to prevent self-injection. // themselves to prevent self-injection.
plugins: [ plugins: [
[ [
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"), path.join(BABEL_PLUGINS, "custom-polyfill-plugin.ts"),
{ method: "usage-global" }, { method: "usage-global" },
], ],
], ],
@@ -221,8 +225,20 @@ const publicPath = (latestBuild, root = "") =>
} }
*/ */
module.exports.config = { export const config = {
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { app({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
isWDS,
}: {
isProdBuild?: boolean;
latestBuild?: boolean;
isStatsBuild?: boolean;
isTestBuild?: boolean;
isWDS?: boolean;
}) {
return { return {
name: "frontend" + nameSuffix(latestBuild), name: "frontend" + nameSuffix(latestBuild),
entry: { entry: {
@@ -257,7 +273,7 @@ module.exports.config = {
outputPath: outputPath(paths.demo_output_root, latestBuild), outputPath: outputPath(paths.demo_output_root, latestBuild),
publicPath: publicPath(latestBuild), publicPath: publicPath(latestBuild),
defineOverlay: { defineOverlay: {
__VERSION__: JSON.stringify(`DEMO-${env.version()}`), __VERSION__: JSON.stringify(`DEMO-${version()}`),
__DEMO__: true, __DEMO__: true,
}, },
isProdBuild, isProdBuild,
@@ -267,7 +283,7 @@ module.exports.config = {
}, },
cast({ isProdBuild, latestBuild }) { cast({ isProdBuild, latestBuild }) {
const entry = { const entry: Record<string, string> = {
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"), launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"), media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
}; };

View File

@@ -1,34 +0,0 @@
const fs = require("fs");
const path = require("path");
const paths = require("./paths.cjs");
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
module.exports = {
isProdBuild() {
return (
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
);
},
isStatsBuild() {
return isTrue(process.env.STATS);
},
isTestBuild() {
return isTrue(process.env.IS_TEST);
},
isNetlify() {
return isTrue(process.env.NETLIFY);
},
version() {
const version = fs
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
if (!version) {
throw Error("Version not found");
}
return version[1];
},
isDevContainer() {
return isTrue(process.env.DEV_CONTAINER);
},
};

21
build-scripts/env.ts Normal file
View File

@@ -0,0 +1,21 @@
import fs from "node:fs";
import path from "node:path";
import paths from "./paths.ts";
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
export const isProdBuild = () =>
process.env.NODE_ENV === "production" || isStatsBuild();
export const isStatsBuild = () => isTrue(process.env.STATS);
export const isTestBuild = () => isTrue(process.env.IS_TEST);
export const isNetlify = () => isTrue(process.env.NETLIFY);
export const version = () => {
const pyProjectVersion = fs
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
if (!pyProjectVersion) {
throw Error("Version not found");
}
return pyProjectVersion[1];
};
export const isDevContainer = () => isTrue(process.env.DEV_CONTAINER);

View File

@@ -1,57 +0,0 @@
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./locale-data.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean",
gulp.parallel(
"gen-service-worker-app-dev",
"gen-icons-json",
"gen-pages-app-dev",
"build-translations",
"build-locale-data"
),
"copy-static-app",
"rspack-watch-app"
)
);
gulp.task(
"build-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app",
"rspack-prod-app",
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
// Don't compress running tests
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
)
);
gulp.task(
"analyze-app",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-app"
)
);

54
build-scripts/gulp/app.ts Normal file
View File

@@ -0,0 +1,54 @@
import { parallel, series } from "gulp";
import { isStatsBuild, isTestBuild } from "../env.ts";
import { clean } from "./clean.ts";
import { compressApp } from "./compress.ts";
import { genPagesAppDev, genPagesAppProd } from "./entry-html.ts";
import { copyStaticApp } from "./gather-static.ts";
import { genIconsJson } from "./gen-icons-json.ts";
import { buildLocaleData } from "./locale-data.ts";
import { rspackProdApp, rspackWatchApp } from "./rspack.ts";
import {
genServiceWorkerAppDev,
genServiceWorkerAppProd,
} from "./service-worker.ts";
import { buildTranslations } from "./translations.ts";
// develop-app
export const developApp = series(
async () => {
process.env.NODE_ENV = "development";
},
clean,
parallel(
genServiceWorkerAppDev,
genIconsJson,
genPagesAppDev,
buildTranslations,
buildLocaleData
),
copyStaticApp,
rspackWatchApp
);
// build-app
export const buildApp = series(
async () => {
process.env.NODE_ENV = "production";
},
clean,
parallel(genIconsJson, buildTranslations, buildLocaleData),
copyStaticApp,
rspackProdApp,
parallel(genPagesAppProd, genServiceWorkerAppProd),
// Don't compress running tests
...(isTestBuild() || isStatsBuild() ? [] : [compressApp])
);
// analyze-app
export const analyzeApp = series(
async () => {
process.env.STATS = "1";
},
clean,
rspackProdApp
);

View File

@@ -1,37 +0,0 @@
import gulp from "gulp";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-cast",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-cast",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"gen-pages-cast-dev",
"rspack-dev-server-cast"
)
);
gulp.task(
"build-cast",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-cast",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"rspack-prod-cast",
"gen-pages-cast-prod"
)
);

View File

@@ -0,0 +1,38 @@
import { parallel, series } from "gulp";
import { cleanCast } from "./clean.ts";
import { genPagesCastDev, genPagesCastProd } from "./entry-html.ts";
import { copyStaticCast } from "./gather-static.ts";
import { genIconsJson } from "./gen-icons-json.ts";
import { buildLocaleData } from "./locale-data.ts";
import { rspackDevServerCast, rspackProdCast } from "./rspack.ts";
import "./service-worker.ts";
import {
buildTranslations,
translationsEnableMergeBackend,
} from "./translations.ts";
// develop-cast
export const developCast = series(
async () => {
process.env.NODE_ENV = "development";
},
cleanCast,
translationsEnableMergeBackend,
parallel(genIconsJson, buildTranslations, buildLocaleData),
copyStaticCast,
genPagesCastDev,
rspackDevServerCast
);
// build-cast
export const buildCast = series(
async () => {
process.env.NODE_ENV = "production";
},
cleanCast,
translationsEnableMergeBackend,
parallel(genIconsJson, buildTranslations, buildLocaleData),
copyStaticCast,
rspackProdCast,
genPagesCastProd
);

View File

@@ -1,51 +0,0 @@
import { deleteSync } from "del";
import gulp from "gulp";
import paths from "../paths.cjs";
import "./translations.js";
gulp.task(
"clean",
gulp.parallel("clean-translations", async () =>
deleteSync([paths.app_output_root, paths.build_dir])
)
);
gulp.task(
"clean-demo",
gulp.parallel("clean-translations", async () =>
deleteSync([paths.demo_output_root, paths.build_dir])
)
);
gulp.task(
"clean-cast",
gulp.parallel("clean-translations", async () =>
deleteSync([paths.cast_output_root, paths.build_dir])
)
);
gulp.task("clean-hassio", async () =>
deleteSync([paths.hassio_output_root, paths.build_dir])
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", async () =>
deleteSync([
paths.gallery_output_root,
paths.gallery_build,
paths.build_dir,
])
)
);
gulp.task(
"clean-landing-page",
gulp.parallel("clean-translations", async () =>
deleteSync([
paths.landingPage_output_root,
paths.landingPage_build,
paths.build_dir,
])
)
);

View File

@@ -0,0 +1,31 @@
import { deleteSync } from "del";
import { parallel } from "gulp";
import paths from "../paths.ts";
import { cleanTranslations } from "./translations.ts";
export const clean = parallel(cleanTranslations, async () =>
deleteSync([paths.app_output_root, paths.build_dir])
);
export const cleanDemo = parallel(cleanTranslations, async () =>
deleteSync([paths.demo_output_root, paths.build_dir])
);
export const cleanCast = parallel(cleanTranslations, async () =>
deleteSync([paths.cast_output_root, paths.build_dir])
);
export const cleanHassio = async () =>
deleteSync([paths.hassio_output_root, paths.build_dir]);
export const cleanGallery = parallel(cleanTranslations, async () =>
deleteSync([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
);
export const cleanLandingPage = parallel(cleanTranslations, async () =>
deleteSync([
paths.landingPage_output_root,
paths.landingPage_build,
paths.build_dir,
])
);

View File

@@ -1,10 +1,10 @@
// Tasks to compress // Tasks to compress
import { constants } from "node:zlib"; import { dest, parallel, src } from "gulp";
import gulp from "gulp";
import brotli from "gulp-brotli"; import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green"; import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs"; import { constants } from "node:zlib";
import paths from "../paths.ts";
const filesGlob = "*.{js,json,css,svg,xml}"; const filesGlob = "*.{js,json,css,svg,xml}";
const brotliOptions = { const brotliOptions = {
@@ -16,27 +16,25 @@ const brotliOptions = {
const zopfliOptions = { threshold: 150 }; const zopfliOptions = { threshold: 150 };
const compressModern = (rootDir, modernDir, compress) => const compressModern = (rootDir, modernDir, compress) =>
gulp src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { base: rootDir,
base: rootDir, allowEmpty: true,
allowEmpty: true, })
})
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions)) .pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
.pipe(gulp.dest(rootDir)); .pipe(dest(rootDir));
const compressOther = (rootDir, modernDir, compress) => const compressOther = (rootDir, modernDir, compress) =>
gulp src(
.src( [
[ `${rootDir}/**/${filesGlob}`,
`${rootDir}/**/${filesGlob}`, `!${modernDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`, `!${rootDir}/{sw-modern,service_worker}.js`,
`!${rootDir}/{sw-modern,service_worker}.js`, `${rootDir}/{authorize,onboarding}.html`,
`${rootDir}/{authorize,onboarding}.html`, ],
], { base: rootDir, allowEmpty: true }
{ base: rootDir, allowEmpty: true } )
)
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions)) .pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
.pipe(gulp.dest(rootDir)); .pipe(dest(rootDir));
const compressAppModernBrotli = () => const compressAppModernBrotli = () =>
compressModern(paths.app_output_root, paths.app_output_latest, "brotli"); compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
@@ -66,21 +64,16 @@ const compressHassioOtherBrotli = () =>
const compressHassioOtherZopfli = () => const compressHassioOtherZopfli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli"); compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
gulp.task( export const compressApp = parallel(
"compress-app", compressAppModernBrotli,
gulp.parallel( compressAppOtherBrotli,
compressAppModernBrotli, compressAppModernZopfli,
compressAppOtherBrotli, compressAppOtherZopfli
compressAppModernZopfli,
compressAppOtherZopfli
)
); );
gulp.task(
"compress-hassio", export const compressHassio = parallel(
gulp.parallel( compressHassioModernBrotli,
compressHassioModernBrotli, compressHassioOtherBrotli,
compressHassioOtherBrotli, compressHassioModernZopfli,
compressHassioModernZopfli, compressHassioOtherZopfli
compressHassioOtherZopfli
)
); );

View File

@@ -1,54 +0,0 @@
import gulp from "gulp";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-demo",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-demo",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"gen-pages-demo-dev",
"build-translations",
"build-locale-data"
),
"copy-static-demo",
"rspack-dev-server-demo"
)
);
gulp.task(
"build-demo",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-demo",
// Cast needs to be backwards compatible and older HA has no translations
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo",
"rspack-prod-demo",
"gen-pages-demo-prod"
)
);
gulp.task(
"analyze-demo",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-demo"
)
);

View File

@@ -0,0 +1,47 @@
import { parallel, series } from "gulp";
import { clean, cleanDemo } from "./clean.ts";
import { genPagesDemoDev, genPagesDemoProd } from "./entry-html.ts";
import { copyStaticDemo } from "./gather-static.ts";
import { genIconsJson } from "./gen-icons-json.ts";
import { buildLocaleData } from "./locale-data.ts";
import { rspackDevServerDemo, rspackProdDemo } from "./rspack.ts";
import "./service-worker.ts";
import {
buildTranslations,
translationsEnableMergeBackend,
} from "./translations.ts";
// develop-demo
export const developDemo = series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
cleanDemo,
translationsEnableMergeBackend,
parallel(genIconsJson, genPagesDemoDev, buildTranslations, buildLocaleData),
copyStaticDemo,
rspackDevServerDemo
);
// build-demo
export const buildDemo = series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
cleanDemo,
// Cast needs to be backwards compatible and older HA has no translations
translationsEnableMergeBackend,
parallel(genIconsJson, buildTranslations, buildLocaleData),
copyStaticDemo,
rspackProdDemo,
genPagesDemoProd
);
// analyze-demo
export const analyzeDemo = series(
async function setEnv() {
process.env.STATS = "1";
},
clean,
rspackProdDemo
);

View File

@@ -1,10 +1,10 @@
import fs from "fs/promises";
import gulp from "gulp";
import path from "path";
import mapStream from "map-stream";
import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api"; import { LokaliseApi } from "@lokalise/node-api";
import { dest, series, src } from "gulp";
import transform from "gulp-json-transform";
import JSZip from "jszip"; import JSZip from "jszip";
import mapStream from "map-stream";
import fs from "node:fs/promises";
import path from "node:path";
const inDir = "translations"; const inDir = "translations";
const inDirFrontend = `${inDir}/frontend`; const inDirFrontend = `${inDir}/frontend`;
@@ -12,11 +12,14 @@ const inDirBackend = `${inDir}/backend`;
const srcMeta = "src/translations/translationMetadata.json"; const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8"; const encoding = "utf8";
function hasHtml(data) { const hasHtml = (data) => /<\S*>/i.test(data);
return /<\S*>/i.test(data);
}
function recursiveCheckHasHtml(file, data, errors, recKey) { const recursiveCheckHasHtml = (
file,
data,
errors: string[],
recKey?: string
) => {
Object.keys(data).forEach(function (key) { Object.keys(data).forEach(function (key) {
if (typeof data[key] === "object") { if (typeof data[key] === "object") {
const nextRecKey = recKey ? `${recKey}.${key}` : key; const nextRecKey = recKey ? `${recKey}.${key}` : key;
@@ -25,9 +28,9 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`); errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
} }
}); });
} };
function checkHtml() { const checkHtml = () => {
const errors = []; const errors = [];
return mapStream(function (file, cb) { return mapStream(function (file, cb) {
@@ -44,9 +47,9 @@ function checkHtml() {
} }
cb(error, file); cb(error, file);
}); });
} };
function convertBackendTranslations(data, _file) { const convertBackendTranslationsTransform = (data, _file) => {
const output = { component: {} }; const output = { component: {} };
if (!data.component) { if (!data.component) {
return output; return output;
@@ -62,25 +65,22 @@ function convertBackendTranslations(data, _file) {
}); });
}); });
return output; return output;
} };
gulp.task("convert-backend-translations", function () { const convertBackendTranslations = () =>
return gulp src([`${inDirBackend}/*.json`])
.src([`${inDirBackend}/*.json`]) .pipe(
.pipe(transform((data, file) => convertBackendTranslations(data, file))) transform((data, file) => convertBackendTranslationsTransform(data, file))
.pipe(gulp.dest(inDirBackend)); )
}); .pipe(dest(inDirBackend));
gulp.task("check-translations-html", function () { const checkTranslationsHtml = () =>
return gulp src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`]).pipe(checkHtml());
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
.pipe(checkHtml());
});
gulp.task("check-all-files-exist", async function () { const checkAllFilesExist = async () => {
const file = await fs.readFile(srcMeta, { encoding }); const file = await fs.readFile(srcMeta, { encoding });
const meta = JSON.parse(file); const meta = JSON.parse(file);
const writings = []; const writings: Promise<void>[] = [];
Object.keys(meta).forEach((lang) => { Object.keys(meta).forEach((lang) => {
writings.push( writings.push(
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), { fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
@@ -92,14 +92,14 @@ gulp.task("check-all-files-exist", async function () {
); );
}); });
await Promise.allSettled(writings); await Promise.allSettled(writings);
}); };
const lokaliseProjects = { const lokaliseProjects = {
backend: "130246255a974bd3b5e8a1.51616605", backend: "130246255a974bd3b5e8a1.51616605",
frontend: "3420425759f6d6d241f598.13594006", frontend: "3420425759f6d6d241f598.13594006",
}; };
gulp.task("fetch-lokalise", async function () { const fetchLokalise = async () => {
let apiKey; let apiKey;
try { try {
apiKey = apiKey =
@@ -168,14 +168,11 @@ gulp.task("fetch-lokalise", async function () {
}) })
) )
); );
}); };
gulp.task( export const downloadTranslations = series(
"download-translations", fetchLokalise,
gulp.series( convertBackendTranslations,
"fetch-lokalise", checkTranslationsHtml,
"convert-backend-translations", checkAllFilesExist
"check-translations-html",
"check-all-files-exist"
)
); );

View File

@@ -6,12 +6,11 @@ import {
getPreUserAgentRegexes, getPreUserAgentRegexes,
} from "browserslist-useragent-regexp"; } from "browserslist-useragent-regexp";
import fs from "fs-extra"; import fs from "fs-extra";
import gulp from "gulp";
import { minify } from "html-minifier-terser"; import { minify } from "html-minifier-terser";
import template from "lodash.template"; import template from "lodash.template";
import { dirname, extname, resolve } from "node:path"; import { dirname, extname, resolve } from "node:path";
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; import { htmlMinifierOptions, terserOptions } from "../bundle.ts";
import paths from "../paths.cjs"; import paths from "../paths.ts";
// macOS companion app has no way to obtain the Safari version used by WKWebView, // macOS companion app has no way to obtain the Safari version used by WKWebView,
// and it is not in the default user agent string. So we add an additional regex // and it is not in the default user agent string. So we add an additional regex
@@ -34,9 +33,9 @@ const getCommonTemplateVars = () => {
mobileToDesktop: true, mobileToDesktop: true,
throwOnMissing: true, throwOnMissing: true,
}); });
const minSafariVersion = browserRegexes.find( const minSafariVersion =
(regex) => regex.family === "safari" browserRegexes.find((regex) => regex.family === "safari")
)?.matchedVersions[0][0]; ?.matchedVersions[0][0] ?? 18;
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion]; const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
if (!minMacOSVersion) { if (!minMacOSVersion) {
throw Error( throw Error(
@@ -106,10 +105,10 @@ const genPagesDevTask =
resolve(inputRoot, inputSub, `${page}.template`), resolve(inputRoot, inputSub, `${page}.template`),
{ {
...commonVars, ...commonVars,
latestEntryJS: entries.map( latestEntryJS: (entries as string[]).map(
(entry) => `${publicRoot}/frontend_latest/${entry}.js` (entry) => `${publicRoot}/frontend_latest/${entry}.js`
), ),
es5EntryJS: entries.map( es5EntryJS: (entries as string[]).map(
(entry) => `${publicRoot}/frontend_es5/${entry}.js` (entry) => `${publicRoot}/frontend_es5/${entry}.js`
), ),
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`, latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
@@ -128,7 +127,7 @@ const genPagesProdTask =
inputRoot, inputRoot,
outputRoot, outputRoot,
outputLatest, outputLatest,
outputES5, outputES5?: string,
inputSub = "src/html" inputSub = "src/html"
) => ) =>
async () => { async () => {
@@ -139,14 +138,18 @@ const genPagesProdTask =
? fs.readJsonSync(resolve(outputES5, "manifest.json")) ? fs.readJsonSync(resolve(outputES5, "manifest.json"))
: {}; : {};
const commonVars = getCommonTemplateVars(); const commonVars = getCommonTemplateVars();
const minifiedHTML = []; const minifiedHTML: Promise<void>[] = [];
for (const [page, entries] of Object.entries(pageEntries)) { for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate( const content = renderTemplate(
resolve(inputRoot, inputSub, `${page}.template`), resolve(inputRoot, inputSub, `${page}.template`),
{ {
...commonVars, ...commonVars,
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), latestEntryJS: (entries as string[]).map(
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), (entry) => latestManifest[`${entry}.js`]
),
es5EntryJS: (entries as string[]).map(
(entry) => es5Manifest[`${entry}.js`]
),
latestCustomPanelJS: latestManifest["custom-panel.js"], latestCustomPanelJS: latestManifest["custom-panel.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"], es5CustomPanelJS: es5Manifest["custom-panel.js"],
} }
@@ -167,20 +170,18 @@ const APP_PAGE_ENTRIES = {
"index.html": ["core", "app"], "index.html": ["core", "app"],
}; };
gulp.task( export const genPagesAppDev = genPagesDevTask(
"gen-pages-app-dev", APP_PAGE_ENTRIES,
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root) paths.root_dir,
paths.app_output_root
); );
gulp.task( export const genPagesAppProd = genPagesProdTask(
"gen-pages-app-prod", APP_PAGE_ENTRIES,
genPagesProdTask( paths.root_dir,
APP_PAGE_ENTRIES, paths.app_output_root,
paths.root_dir, paths.app_output_latest,
paths.app_output_root, paths.app_output_es5
paths.app_output_latest,
paths.app_output_es5
)
); );
const CAST_PAGE_ENTRIES = { const CAST_PAGE_ENTRIES = {
@@ -190,104 +191,82 @@ const CAST_PAGE_ENTRIES = {
"receiver.html": ["receiver"], "receiver.html": ["receiver"],
}; };
gulp.task( export const genPagesCastDev = genPagesDevTask(
"gen-pages-cast-dev", CAST_PAGE_ENTRIES,
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root) paths.cast_dir,
paths.cast_output_root
); );
gulp.task( export const genPagesCastProd = genPagesProdTask(
"gen-pages-cast-prod", CAST_PAGE_ENTRIES,
genPagesProdTask( paths.cast_dir,
CAST_PAGE_ENTRIES, paths.cast_output_root,
paths.cast_dir, paths.cast_output_latest,
paths.cast_output_root, paths.cast_output_es5
paths.cast_output_latest,
paths.cast_output_es5
)
); );
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] }; const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
gulp.task( export const genPagesDemoDev = genPagesDevTask(
"gen-pages-demo-dev", DEMO_PAGE_ENTRIES,
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root) paths.demo_dir,
paths.demo_output_root
); );
gulp.task( export const genPagesDemoProd = genPagesProdTask(
"gen-pages-demo-prod", DEMO_PAGE_ENTRIES,
genPagesProdTask( paths.demo_dir,
DEMO_PAGE_ENTRIES, paths.demo_output_root,
paths.demo_dir, paths.demo_output_latest,
paths.demo_output_root, paths.demo_output_es5
paths.demo_output_latest,
paths.demo_output_es5
)
); );
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] }; const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task( export const genPagesGalleryDev = genPagesDevTask(
"gen-pages-gallery-dev", GALLERY_PAGE_ENTRIES,
genPagesDevTask( paths.gallery_dir,
GALLERY_PAGE_ENTRIES, paths.gallery_output_root
paths.gallery_dir,
paths.gallery_output_root
)
); );
gulp.task( export const genPagesGalleryProd = genPagesProdTask(
"gen-pages-gallery-prod", GALLERY_PAGE_ENTRIES,
genPagesProdTask( paths.gallery_dir,
GALLERY_PAGE_ENTRIES, paths.gallery_output_root,
paths.gallery_dir, paths.gallery_output_latest
paths.gallery_output_root,
paths.gallery_output_latest
)
); );
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] }; const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task( export const genPagesLandingPageDev = genPagesDevTask(
"gen-pages-landing-page-dev", LANDING_PAGE_PAGE_ENTRIES,
genPagesDevTask( paths.landingPage_dir,
LANDING_PAGE_PAGE_ENTRIES, paths.landingPage_output_root
paths.landingPage_dir,
paths.landingPage_output_root
)
); );
gulp.task( export const genPagesLandingPageProd = genPagesProdTask(
"gen-pages-landing-page-prod", LANDING_PAGE_PAGE_ENTRIES,
genPagesProdTask( paths.landingPage_dir,
LANDING_PAGE_PAGE_ENTRIES, paths.landingPage_output_root,
paths.landingPage_dir, paths.landingPage_output_latest,
paths.landingPage_output_root, paths.landingPage_output_es5
paths.landingPage_output_latest,
paths.landingPage_output_es5
)
); );
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] }; const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task( export const genPagesHassioDev = genPagesDevTask(
"gen-pages-hassio-dev", HASSIO_PAGE_ENTRIES,
genPagesDevTask( paths.hassio_dir,
HASSIO_PAGE_ENTRIES, paths.hassio_output_root,
paths.hassio_dir, "src",
paths.hassio_output_root, paths.hassio_publicPath
"src",
paths.hassio_publicPath
)
); );
gulp.task( export const genPagesHassioProd = genPagesProdTask(
"gen-pages-hassio-prod", HASSIO_PAGE_ENTRIES,
genPagesProdTask( paths.hassio_dir,
HASSIO_PAGE_ENTRIES, paths.hassio_output_root,
paths.hassio_dir, paths.hassio_output_latest,
paths.hassio_output_root, paths.hassio_output_es5,
paths.hassio_output_latest, "src"
paths.hassio_output_es5,
"src"
)
); );

View File

@@ -1,14 +1,14 @@
// Task to download the latest Lokalise translations from the nightly workflow artifacts // Task to download the latest 00Lokalise translations from the nightly workflow artifacts
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device"; import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
import { retry } from "@octokit/plugin-retry"; import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { deleteAsync } from "del"; import { deleteAsync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises"; import { series } from "gulp";
import gulp from "gulp";
import jszip from "jszip"; import jszip from "jszip";
import path from "path"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import process from "process"; import path from "node:path";
import process from "node:process";
import { extract } from "tar"; import { extract } from "tar";
const MAX_AGE = 24; // hours const MAX_AGE = 24; // hours
@@ -22,12 +22,13 @@ const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json"); const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
let allowTokenSetup = false; let allowTokenSetup = false;
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
export const allowSetupFetchNightlyTranslations = (done) => {
allowTokenSetup = true; allowTokenSetup = true;
done(); done();
}); };
gulp.task("fetch-nightly-translations", async function () { export const fetchNightlyTranslations = async () => {
// Skip all when environment flag is set (assumes translations are already in place) // Skip all when environment flag is set (assumes translations are already in place)
if (process.env?.SKIP_FETCH_NIGHTLY_TRANSLATIONS) { if (process.env?.SKIP_FETCH_NIGHTLY_TRANSLATIONS) {
console.log("Skipping fetch due to environment signal"); console.log("Skipping fetch due to environment signal");
@@ -54,7 +55,7 @@ gulp.task("fetch-nightly-translations", async function () {
// To store file writing promises // To store file writing promises
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true }); const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
const writings = []; const writings: Promise<void>[] = [];
// Authenticate to GitHub using GitHub action token if it exists, // Authenticate to GitHub using GitHub action token if it exists,
// otherwise look for a saved user token or generate a new one if none // otherwise look for a saved user token or generate a new one if none
@@ -87,7 +88,7 @@ gulp.task("fetch-nightly-translations", async function () {
}); });
tokenAuth = await auth({ type: "oauth" }); tokenAuth = await auth({ type: "oauth" });
writings.push( writings.push(
createExtractDir.then( createExtractDir.then(() =>
writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2)) writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
) )
); );
@@ -131,13 +132,13 @@ gulp.task("fetch-nightly-translations", async function () {
throw Error("Latest nightly workflow run has no translations artifact"); throw Error("Latest nightly workflow run has no translations artifact");
} }
writings.push( writings.push(
createExtractDir.then( createExtractDir.then(() =>
writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2)) writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
) )
); );
// Remove the current translations // Remove the current translations
const deleteCurrent = Promise.all(writings).then( const deleteCurrent = Promise.all(writings).then(() =>
deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`]) deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
); );
@@ -148,24 +149,22 @@ gulp.task("fetch-nightly-translations", async function () {
artifact_id: latestArtifact.id, artifact_id: latestArtifact.id,
archive_format: "zip", archive_format: "zip",
}); });
// @ts-ignore OctokitResponse<unknown, 302> doesn't allow to check for 200
if (downloadResponse.status !== 200) { if (downloadResponse.status !== 200) {
throw Error("Failure downloading translations artifact"); throw Error("Failure downloading translations artifact");
} }
// Artifact is a tarball, but GitHub adds it to a zip file // Artifact is a tarball, but GitHub adds it to a zip file
console.log("Unpacking downloaded translations..."); console.log("Unpacking downloaded translations...");
const zip = await jszip.loadAsync(downloadResponse.data); const zip = await jszip.loadAsync(downloadResponse.data as any);
await deleteCurrent; await deleteCurrent;
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
extractStream.on("close", resolve).on("error", reject); extractStream.on("close", resolve).on("error", reject);
}); });
}); };
gulp.task( export const setupAndFetchNightlyTranslations = series(
"setup-and-fetch-nightly-translations", allowSetupFetchNightlyTranslations,
gulp.series( fetchNightlyTranslations
"allow-setup-fetch-nightly-translations",
"fetch-nightly-translations"
)
); );

View File

@@ -1,19 +1,23 @@
import fs from "fs";
import { glob } from "glob"; import { glob } from "glob";
import gulp from "gulp"; import { parallel, series, watch } from "gulp";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { marked } from "marked"; import { marked } from "marked";
import path from "path"; import fs from "node:fs";
import paths from "../paths.cjs"; import path from "node:path";
import "./clean.js"; import paths from "../paths.ts";
import "./entry-html.js"; import { cleanGallery } from "./clean.ts";
import "./gather-static.js"; import { genPagesGalleryDev, genPagesGalleryProd } from "./entry-html.ts";
import "./gen-icons-json.js"; import { copyStaticGallery } from "./gather-static.ts";
import "./service-worker.js"; import { genIconsJson } from "./gen-icons-json.ts";
import "./translations.js"; import { buildLocaleData } from "./locale-data.ts";
import "./rspack.js"; import { rspackDevServerGallery, rspackProdGallery } from "./rspack.ts";
import {
buildTranslations,
translationsEnableMergeBackend,
} from "./translations.ts";
gulp.task("gather-gallery-pages", async function gatherPages() { // gather-gallery-pages
export const gatherGalleryPages = async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages"); const pageDir = path.resolve(paths.gallery_dir, "src/pages");
const files = await glob(path.resolve(pageDir, "**/*")); const files = await glob(path.resolve(pageDir, "**/*"));
@@ -22,7 +26,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
let content = "export const PAGES = {\n"; let content = "export const PAGES = {\n";
const processed = new Set(); const processed = new Set<string>();
for (const file of files) { for (const file of files) {
if (fs.lstatSync(file).isDirectory()) { if (fs.lstatSync(file).isDirectory()) {
@@ -47,7 +51,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
if (descriptionContent.startsWith("---")) { if (descriptionContent.startsWith("---")) {
const metadataEnd = descriptionContent.indexOf("---", 3); const metadataEnd = descriptionContent.indexOf("---", 3);
metadata = yaml.load(descriptionContent.substring(3, metadataEnd)); metadata = yaml.load(
descriptionContent.substring(3, metadataEnd)
) as any;
descriptionContent = descriptionContent descriptionContent = descriptionContent
.substring(metadataEnd + 3) .substring(metadataEnd + 3)
.trim(); .trim();
@@ -57,7 +63,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
if (descriptionContent === "") { if (descriptionContent === "") {
hasDescription = false; hasDescription = false;
} else { } else {
descriptionContent = marked(descriptionContent).replace(/`/g, "\\`"); // eslint-disable-next-line no-await-in-loop
descriptionContent = await marked(descriptionContent);
descriptionContent = descriptionContent.replace(/`/g, "\\`");
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true }); fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
fs.writeFileSync( fs.writeFileSync(
path.resolve(galleryBuild, `${pageId}-description.ts`), path.resolve(galleryBuild, `${pageId}-description.ts`),
@@ -95,7 +103,10 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
pagesToProcess[category].add(page); pagesToProcess[category].add(page);
} }
for (const group of Object.values(sidebar)) { for (const group of Object.values(sidebar) as {
category: string;
pages?: string[];
}[]) {
const toProcess = pagesToProcess[group.category]; const toProcess = pagesToProcess[group.category];
delete pagesToProcess[group.category]; delete pagesToProcess[group.category];
@@ -118,7 +129,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
group.pages = []; group.pages = [];
} }
for (const page of Array.from(toProcess).sort()) { for (const page of Array.from(toProcess).sort()) {
group.pages.push(page); group.pages.push(page as string);
} }
} }
@@ -126,7 +137,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
sidebar.push({ sidebar.push({
category, category,
header: category, header: category,
pages: Array.from(pages).sort(), pages: Array.from(pages as Set<string>).sort(),
}); });
} }
@@ -137,55 +148,48 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
content, content,
"utf-8" "utf-8"
); );
}); };
gulp.task( // develop-gallery
"develop-gallery", export const developGallery = series(
gulp.series( async function setEnv() {
async function setEnv() { process.env.NODE_ENV = "development";
process.env.NODE_ENV = "development"; },
}, cleanGallery,
"clean-gallery", translationsEnableMergeBackend,
"translations-enable-merge-backend", parallel(
gulp.parallel( genIconsJson,
"gen-icons-json", buildTranslations,
"build-translations", buildLocaleData,
"build-locale-data", gatherGalleryPages
"gather-gallery-pages" ),
), copyStaticGallery,
"copy-static-gallery", genPagesGalleryDev,
"gen-pages-gallery-dev", parallel(rspackDevServerGallery, async function watchMarkdownFiles() {
gulp.parallel( watch(
"rspack-dev-server-gallery", [
async function watchMarkdownFiles() { path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"),
gulp.watch( path.resolve(paths.gallery_dir, "sidebar.js"),
[ ],
path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"), series(gatherGalleryPages)
path.resolve(paths.gallery_dir, "sidebar.js"), );
], })
gulp.series("gather-gallery-pages")
);
}
)
)
); );
gulp.task( // build-gallery
"build-gallery", export const buildGallery = series(
gulp.series( async function setEnv() {
async function setEnv() { process.env.NODE_ENV = "production";
process.env.NODE_ENV = "production"; },
}, cleanGallery,
"clean-gallery", translationsEnableMergeBackend,
"translations-enable-merge-backend", parallel(
gulp.parallel( genIconsJson,
"gen-icons-json", buildTranslations,
"build-translations", buildLocaleData,
"build-locale-data", gatherGalleryPages
"gather-gallery-pages" ),
), copyStaticGallery,
"copy-static-gallery", rspackProdGallery,
"rspack-prod-gallery", genPagesGalleryProd
"gen-pages-gallery-prod"
)
); );

View File

@@ -1,9 +1,8 @@
// Gulp task to gather all static files. // Gulp task to gather all static files.
import fs from "fs-extra"; import fs from "fs-extra";
import gulp from "gulp"; import path from "node:path";
import path from "path"; import paths from "../paths.ts";
import paths from "../paths.cjs";
const npmPath = (...parts) => const npmPath = (...parts) =>
path.resolve(paths.root_dir, "node_modules", ...parts); path.resolve(paths.root_dir, "node_modules", ...parts);
@@ -17,7 +16,7 @@ const genStaticPath =
(...parts) => (...parts) =>
path.resolve(staticDir, ...parts); path.resolve(staticDir, ...parts);
function copyTranslations(staticDir) { const copyTranslations = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Translation output // Translation output
@@ -25,23 +24,23 @@ function copyTranslations(staticDir) {
polyPath("build/translations/output"), polyPath("build/translations/output"),
staticPath("translations") staticPath("translations")
); );
} };
function copyLocaleData(staticDir) { const copyLocaleData = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Locale data output // Locale data output
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data")); fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
} };
function copyMdiIcons(staticDir) { const copyMdiIcons = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// MDI icons output // MDI icons output
fs.copySync(polyPath("build/mdi"), staticPath("mdi")); fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
} };
function copyPolyfills(staticDir) { const copyPolyfills = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// For custom panels using ES5 builds that don't use Babel 7+ // For custom panels using ES5 builds that don't use Babel 7+
@@ -70,9 +69,9 @@ function copyPolyfills(staticDir) {
npmPath("dialog-polyfill/dialog-polyfill.css"), npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/") staticPath("polyfills/")
); );
} };
function copyFonts(staticDir) { const copyFonts = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Local fonts // Local fonts
fs.copySync( fs.copySync(
@@ -82,14 +81,14 @@ function copyFonts(staticDir) {
filter: (src) => !src.includes(".") || src.endsWith(".woff2"), filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
} }
); );
} };
function copyQrScannerWorker(staticDir) { const copyQrScannerWorker = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js")); copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
} };
function copyMapPanel(staticDir) { const copyMapPanel = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir( copyFileDir(
npmPath("leaflet/dist/leaflet.css"), npmPath("leaflet/dist/leaflet.css"),
@@ -103,43 +102,38 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/images"), npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/") staticPath("images/leaflet/images/")
); );
} };
function copyZXingWasm(staticDir) { const copyZXingWasm = (staticDir) => {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir( copyFileDir(
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"), npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
staticPath("js") staticPath("js")
); );
} };
gulp.task("copy-locale-data", async () => { export const copyTranslationsApp = async () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
});
gulp.task("copy-translations-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
copyTranslations(staticDir); copyTranslations(staticDir);
}); };
gulp.task("copy-translations-supervisor", async () => { export const copyTranslationsSupervisor = async () => {
const staticDir = paths.hassio_output_static; const staticDir = paths.hassio_output_static;
copyTranslations(staticDir); copyTranslations(staticDir);
}); };
gulp.task("copy-translations-landing-page", async () => { export const copyTranslationsLandingPage = async () => {
const staticDir = paths.landingPage_output_static; const staticDir = paths.landingPage_output_static;
copyTranslations(staticDir); copyTranslations(staticDir);
}); };
gulp.task("copy-static-supervisor", async () => { export const copyStaticSupervisor = async () => {
const staticDir = paths.hassio_output_static; const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir); copyLocaleData(staticDir);
copyFonts(staticDir); copyFonts(staticDir);
}); };
gulp.task("copy-static-app", async () => { export const copyStaticApp = async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
// Basic static files // Basic static files
fs.copySync(polyPath("public"), paths.app_output_root); fs.copySync(polyPath("public"), paths.app_output_root);
@@ -155,9 +149,9 @@ gulp.task("copy-static-app", async () => {
// Qr Scanner assets // Qr Scanner assets
copyZXingWasm(staticDir); copyZXingWasm(staticDir);
copyQrScannerWorker(staticDir); copyQrScannerWorker(staticDir);
}); };
gulp.task("copy-static-demo", async () => { export const copyStaticDemo = async () => {
// Copy app static files // Copy app static files
fs.copySync( fs.copySync(
polyPath("public/static"), polyPath("public/static"),
@@ -171,9 +165,9 @@ gulp.task("copy-static-demo", async () => {
copyTranslations(paths.demo_output_static); copyTranslations(paths.demo_output_static);
copyLocaleData(paths.demo_output_static); copyLocaleData(paths.demo_output_static);
copyMdiIcons(paths.demo_output_static); copyMdiIcons(paths.demo_output_static);
}); };
gulp.task("copy-static-cast", async () => { export const copyStaticCast = async () => {
// Copy app static files // Copy app static files
fs.copySync(polyPath("public/static"), paths.cast_output_static); fs.copySync(polyPath("public/static"), paths.cast_output_static);
// Copy cast static files // Copy cast static files
@@ -184,9 +178,9 @@ gulp.task("copy-static-cast", async () => {
copyTranslations(paths.cast_output_static); copyTranslations(paths.cast_output_static);
copyLocaleData(paths.cast_output_static); copyLocaleData(paths.cast_output_static);
copyMdiIcons(paths.cast_output_static); copyMdiIcons(paths.cast_output_static);
}); };
gulp.task("copy-static-gallery", async () => { export const copyStaticGallery = async () => {
// Copy app static files // Copy app static files
fs.copySync(polyPath("public/static"), paths.gallery_output_static); fs.copySync(polyPath("public/static"), paths.gallery_output_static);
// Copy gallery static files // Copy gallery static files
@@ -200,9 +194,9 @@ gulp.task("copy-static-gallery", async () => {
copyTranslations(paths.gallery_output_static); copyTranslations(paths.gallery_output_static);
copyLocaleData(paths.gallery_output_static); copyLocaleData(paths.gallery_output_static);
copyMdiIcons(paths.gallery_output_static); copyMdiIcons(paths.gallery_output_static);
}); };
gulp.task("copy-static-landing-page", async () => { export const copyStaticLandingPage = async () => {
// Copy landing-page static files // Copy landing-page static files
fs.copySync( fs.copySync(
path.resolve(paths.landingPage_dir, "public"), path.resolve(paths.landingPage_dir, "public"),
@@ -211,4 +205,4 @@ gulp.task("copy-static-landing-page", async () => {
copyFonts(paths.landingPage_output_static); copyFonts(paths.landingPage_output_static);
copyTranslations(paths.landingPage_output_static); copyTranslations(paths.landingPage_output_static);
}); };

View File

@@ -1,8 +1,7 @@
import fs from "fs"; import fs from "node:fs";
import gulp from "gulp"; import path from "node:path";
import hash from "object-hash"; import hash from "object-hash";
import path from "path"; import paths from "../paths.ts";
import paths from "../paths.cjs";
const ICON_PACKAGE_PATH = path.resolve("node_modules/@mdi/svg/"); const ICON_PACKAGE_PATH = path.resolve("node_modules/@mdi/svg/");
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json"); const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
@@ -21,7 +20,7 @@ const getMeta = () => {
encoding, encoding,
}); });
return { return {
path: svg.match(/ d="([^"]+)"/)[1], path: svg.match(/ d="([^"]+)"/)?.[1],
name: icon.name, name: icon.name,
tags: icon.tags, tags: icon.tags,
aliases: icon.aliases, aliases: icon.aliases,
@@ -55,14 +54,14 @@ const orderMeta = (meta) => {
}; };
const splitBySize = (meta) => { const splitBySize = (meta) => {
const chunks = []; const chunks: any[] = [];
const CHUNK_SIZE = 50000; const CHUNK_SIZE = 50000;
let curSize = 0; let curSize = 0;
let startKey; let startKey;
let icons = []; let icons: any[] = [];
Object.values(meta).forEach((icon) => { Object.values(meta).forEach((icon: any) => {
if (startKey === undefined) { if (startKey === undefined) {
startKey = icon.name; startKey = icon.name;
} }
@@ -94,10 +93,10 @@ const findDifferentiator = (curString, prevString) => {
return curString.substring(0, i + 1); return curString.substring(0, i + 1);
} }
} }
throw new Error("Cannot find differentiator", curString, prevString); throw new Error(`Cannot find differentiator; ${curString}; ${prevString}`);
}; };
gulp.task("gen-icons-json", (done) => { export const genIconsJson = (done) => {
const meta = getMeta(); const meta = getMeta();
const metaAndRemoved = addRemovedMeta(meta); const metaAndRemoved = addRemovedMeta(meta);
@@ -106,7 +105,7 @@ gulp.task("gen-icons-json", (done) => {
if (!fs.existsSync(OUTPUT_DIR)) { if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.mkdirSync(OUTPUT_DIR, { recursive: true });
} }
const parts = []; const parts: any[] = [];
let lastEnd; let lastEnd;
split.forEach((chunk) => { split.forEach((chunk) => {
@@ -153,13 +152,13 @@ gulp.task("gen-icons-json", (done) => {
); );
done(); done();
}); };
gulp.task("gen-dummy-icons-json", (done) => { export const genDummyIconsJson = (done) => {
if (!fs.existsSync(OUTPUT_DIR)) { if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.mkdirSync(OUTPUT_DIR, { recursive: true });
} }
fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]"); fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]");
done(); done();
}); };

View File

@@ -1,45 +0,0 @@
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-dummy-icons-json",
"gen-pages-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])
)
);

View File

@@ -0,0 +1,45 @@
import { series } from "gulp";
import { isTestBuild } from "../env.ts";
import { cleanHassio } from "./clean.ts";
import { compressHassio } from "./compress.ts";
import { genPagesHassioDev, genPagesHassioProd } from "./entry-html.ts";
import {
copyStaticSupervisor,
copyTranslationsSupervisor,
} from "./gather-static.ts";
import { genDummyIconsJson } from "./gen-icons-json.ts";
import { buildLocaleData } from "./locale-data.ts";
import { rspackProdHassio, rspackWatchHassio } from "./rspack.ts";
import { buildSupervisorTranslations } from "./translations.ts";
// develop-hassio
export const developHassio = series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
cleanHassio,
genDummyIconsJson,
genPagesHassioDev,
buildSupervisorTranslations,
copyTranslationsSupervisor,
buildLocaleData,
copyStaticSupervisor,
rspackWatchHassio
);
// build-hassio
export const buildHassio = series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
cleanHassio,
genDummyIconsJson,
buildSupervisorTranslations,
copyTranslationsSupervisor,
buildLocaleData,
copyStaticSupervisor,
rspackProdHassio,
genPagesHassioProd,
...// Don't compress running tests
(isTestBuild() ? [] : [compressHassio])
);

View File

@@ -1,17 +0,0 @@
import "./app.js";
import "./cast.js";
import "./clean.js";
import "./compress.js";
import "./demo.js";
import "./download-translations.js";
import "./entry-html.js";
import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./hassio.js";
import "./landing-page.js";
import "./locale-data.js";
import "./rspack.js";
import "./service-worker.js";
import "./translations.js";

View File

@@ -0,0 +1,42 @@
import { analyzeApp, buildApp, developApp } from "./app";
import { buildCast, developCast } from "./cast";
import { analyzeDemo, buildDemo, developDemo } from "./demo";
import { downloadTranslations } from "./download-translations";
import { setupAndFetchNightlyTranslations } from "./fetch-nightly-translations";
import { buildGallery, developGallery, gatherGalleryPages } from "./gallery";
import { genIconsJson } from "./gen-icons-json";
import { buildHassio, developHassio } from "./hassio";
import { buildLandingPage, developLandingPage } from "./landing-page";
import { buildLocaleData } from "./locale-data";
import { buildTranslations } from "./translations";
export default {
"develop-app": developApp,
"build-app": buildApp,
"analyze-app": analyzeApp,
"develop-cast": developCast,
"build-cast": buildCast,
"develop-demo": developDemo,
"build-demo": buildDemo,
"analyze-demo": analyzeDemo,
"develop-gallery": developGallery,
"build-gallery": buildGallery,
"gather-gallery-pages": gatherGalleryPages,
"develop-hassio": developHassio,
"build-hassio": buildHassio,
"develop-landing-page": developLandingPage,
"build-landing-page": buildLandingPage,
"setup-and-fetch-nightly-translations": setupAndFetchNightlyTranslations,
"download-translations": downloadTranslations,
"build-translations": buildTranslations,
"gen-icons-json": genIconsJson,
"build-locale-data": buildLocaleData,
};

View File

@@ -1,41 +0,0 @@
import gulp from "gulp";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-landing-page",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-landing-page",
"translations-enable-merge-backend",
"build-landing-page-translations",
"copy-translations-landing-page",
"build-locale-data",
"copy-static-landing-page",
"gen-pages-landing-page-dev",
"rspack-watch-landing-page"
)
);
gulp.task(
"build-landing-page",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-landing-page",
"build-landing-page-translations",
"copy-translations-landing-page",
"build-locale-data",
"copy-static-landing-page",
"rspack-prod-landing-page",
"gen-pages-landing-page-prod"
)
);

View File

@@ -0,0 +1,46 @@
import { series } from "gulp";
import { cleanLandingPage } from "./clean.ts";
import "./compress.ts";
import {
genPagesLandingPageDev,
genPagesLandingPageProd,
} from "./entry-html.ts";
import {
copyStaticLandingPage,
copyTranslationsLandingPage,
} from "./gather-static.ts";
import { buildLocaleData } from "./locale-data.ts";
import { rspackProdLandingPage, rspackWatchLandingPage } from "./rspack.ts";
import {
buildLandingPageTranslations,
translationsEnableMergeBackend,
} from "./translations.ts";
// develop-landing-page
export const developLandingPage = series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
cleanLandingPage,
translationsEnableMergeBackend,
buildLandingPageTranslations,
copyTranslationsLandingPage,
buildLocaleData,
copyStaticLandingPage,
genPagesLandingPageDev,
rspackWatchLandingPage
);
// build-landing-page
export const buildLandingPage = series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
cleanLandingPage,
buildLandingPageTranslations,
copyTranslationsLandingPage,
buildLocaleData,
copyStaticLandingPage,
rspackProdLandingPage,
genPagesLandingPageProd
);

View File

@@ -1,8 +1,8 @@
import { deleteSync } from "del"; import { deleteSync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises"; import { series } from "gulp";
import gulp from "gulp"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path"; import { join, resolve } from "node:path";
import paths from "../paths.cjs"; import paths from "../paths.ts";
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs"); const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
const outDir = join(paths.build_dir, "locale-data"); const outDir = join(paths.build_dir, "locale-data");
@@ -31,7 +31,7 @@ const convertToJSON = async (
join(formatjsDir, pkg, subDir, `${language}.js`), join(formatjsDir, pkg, subDir, `${language}.js`),
"utf-8" "utf-8"
); );
} catch (e) { } catch (e: any) {
// Ignore if language is missing (i.e. not supported by @formatjs) // Ignore if language is missing (i.e. not supported by @formatjs)
if (e.code === "ENOENT" && skipMissing) { if (e.code === "ENOENT" && skipMissing) {
console.warn(`Skipped missing data for language ${lang} from ${pkg}`); console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
@@ -54,16 +54,16 @@ const convertToJSON = async (
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData); await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
}; };
gulp.task("clean-locale-data", async () => deleteSync([outDir])); const cleanLocaleData = async () => deleteSync([outDir]);
gulp.task("create-locale-data", async () => { const createLocaleData = async () => {
const translationMeta = JSON.parse( const translationMeta = JSON.parse(
await readFile( await readFile(
resolve(paths.translations_src, "translationMetadata.json"), resolve(paths.translations_src, "translationMetadata.json"),
"utf-8" "utf-8"
) )
); );
const conversions = []; const conversions: any[] = [];
for (const pkg of Object.keys(INTL_POLYFILLS)) { for (const pkg of Object.keys(INTL_POLYFILLS)) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await mkdir(join(outDir, pkg), { recursive: true }); await mkdir(join(outDir, pkg), { recursive: true });
@@ -81,9 +81,6 @@ gulp.task("create-locale-data", async () => {
) )
); );
await Promise.all(conversions); await Promise.all(conversions);
}); };
gulp.task( export const buildLocaleData = series(cleanLocaleData, createLocaleData);
"build-locale-data",
gulp.series("clean-locale-data", "create-locale-data")
);

View File

@@ -1,13 +1,13 @@
// Tasks to run rspack. // Tasks to run rspack.
import fs from "fs";
import path from "path";
import log from "fancy-log";
import gulp from "gulp";
import rspack from "@rspack/core"; import rspack from "@rspack/core";
import { RspackDevServer } from "@rspack/dev-server"; import { RspackDevServer } from "@rspack/dev-server";
import env from "../env.cjs"; import log from "fancy-log";
import paths from "../paths.cjs"; import { series, watch } from "gulp";
import fs from "node:fs";
import path from "node:path";
import { isDevContainer, isStatsBuild, isTestBuild } from "../env.ts";
import paths from "../paths.ts";
import { import {
createAppConfig, createAppConfig,
createCastConfig, createCastConfig,
@@ -15,7 +15,17 @@ import {
createGalleryConfig, createGalleryConfig,
createHassioConfig, createHassioConfig,
createLandingPageConfig, createLandingPageConfig,
} from "../rspack.cjs"; } from "../rspack.ts";
import {
copyTranslationsApp,
copyTranslationsLandingPage,
copyTranslationsSupervisor,
} from "./gather-static.ts";
import {
buildLandingPageTranslations,
buildSupervisorTranslations,
buildTranslations,
} from "./translations.ts";
const bothBuilds = (createConfigFunc, params) => [ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }), createConfigFunc({ ...params, latestBuild: true }),
@@ -29,6 +39,14 @@ const isWsl =
.toLocaleLowerCase() .toLocaleLowerCase()
.includes("microsoft"); .includes("microsoft");
interface RunDevServer {
compiler: any;
contentBase: string;
port: number;
listenHost?: string;
proxy?: any;
}
/** /**
* @param {{ * @param {{
* compiler: import("@rspack/core").Compiler, * compiler: import("@rspack/core").Compiler,
@@ -41,12 +59,12 @@ const runDevServer = async ({
compiler, compiler,
contentBase, contentBase,
port, port,
listenHost = undefined, listenHost,
proxy = undefined, proxy,
}) => { }: RunDevServer) => {
if (listenHost === undefined) { if (listenHost === undefined) {
// For dev container, we need to listen on all hosts // For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost"; listenHost = isDevContainer() ? "0.0.0.0" : "localhost";
} }
const server = new RspackDevServer( const server = new RspackDevServer(
{ {
@@ -68,7 +86,7 @@ const runDevServer = async ({
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`); log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
}; };
const doneHandler = (done) => (err, stats) => { const doneHandler = (done?: (value?: unknown) => void) => (err, stats) => {
if (err) { if (err) {
log.error(err.stack || err); log.error(err.stack || err);
if (err.details) { if (err.details) {
@@ -97,49 +115,46 @@ const prodBuild = (conf) =>
); );
}); });
gulp.task("rspack-watch-app", () => { export const rspackWatchApp = () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
rspack( rspack(
process.env.ES5 process.env.ES5
? bothBuilds(createAppConfig, { isProdBuild: false }) ? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true }) : createAppConfig({ isProdBuild: false, latestBuild: true })
).watch({ poll: isWsl }, doneHandler()); ).watch({ poll: isWsl }, doneHandler());
gulp.watch( watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations-app") series(buildTranslations, copyTranslationsApp)
); );
}); };
gulp.task("rspack-prod-app", () => export const rspackProdApp = () =>
prodBuild( prodBuild(
bothBuilds(createAppConfig, { bothBuilds(createAppConfig, {
isProdBuild: true, isProdBuild: true,
isStatsBuild: env.isStatsBuild(), isStatsBuild: isStatsBuild(),
isTestBuild: env.isTestBuild(), isTestBuild: isTestBuild(),
}) })
) );
);
gulp.task("rspack-dev-server-demo", () => export const rspackDevServerDemo = () =>
runDevServer({ runDevServer({
compiler: rspack( compiler: rspack(
createDemoConfig({ isProdBuild: false, latestBuild: true }) createDemoConfig({ isProdBuild: false, latestBuild: true })
), ),
contentBase: paths.demo_output_root, contentBase: paths.demo_output_root,
port: 8090, port: 8090,
}) });
);
gulp.task("rspack-prod-demo", () => export const rspackProdDemo = () =>
prodBuild( prodBuild(
bothBuilds(createDemoConfig, { bothBuilds(createDemoConfig, {
isProdBuild: true, isProdBuild: true,
isStatsBuild: env.isStatsBuild(), isStatsBuild: isStatsBuild(),
}) })
) );
);
gulp.task("rspack-dev-server-cast", () => export const rspackDevServerCast = () =>
runDevServer({ runDevServer({
compiler: rspack( compiler: rspack(
createCastConfig({ isProdBuild: false, latestBuild: true }) createCastConfig({ isProdBuild: false, latestBuild: true })
@@ -148,18 +163,16 @@ gulp.task("rspack-dev-server-cast", () =>
port: 8080, port: 8080,
// Accessible from the network, because that's how Cast hits it. // Accessible from the network, because that's how Cast hits it.
listenHost: "0.0.0.0", listenHost: "0.0.0.0",
}) });
);
gulp.task("rspack-prod-cast", () => export const rspackProdCast = () =>
prodBuild( prodBuild(
bothBuilds(createCastConfig, { bothBuilds(createCastConfig, {
isProdBuild: true, isProdBuild: true,
}) })
) );
);
gulp.task("rspack-watch-hassio", () => { export const rspackWatchHassio = () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
rspack( rspack(
createHassioConfig({ createHassioConfig({
@@ -168,23 +181,22 @@ gulp.task("rspack-watch-hassio", () => {
}) })
).watch({ ignored: /build/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build/, poll: isWsl }, doneHandler());
gulp.watch( watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor") series(buildSupervisorTranslations, copyTranslationsSupervisor)
); );
}); };
gulp.task("rspack-prod-hassio", () => export const rspackProdHassio = () =>
prodBuild( prodBuild(
bothBuilds(createHassioConfig, { bothBuilds(createHassioConfig, {
isProdBuild: true, isProdBuild: true,
isStatsBuild: env.isStatsBuild(), isStatsBuild: isStatsBuild(),
isTestBuild: env.isTestBuild(), isTestBuild: isTestBuild(),
}) })
) );
);
gulp.task("rspack-dev-server-gallery", () => export const rspackDevServerGallery = () =>
runDevServer({ runDevServer({
compiler: rspack( compiler: rspack(
createGalleryConfig({ isProdBuild: false, latestBuild: true }) createGalleryConfig({ isProdBuild: false, latestBuild: true })
@@ -192,19 +204,17 @@ gulp.task("rspack-dev-server-gallery", () =>
contentBase: paths.gallery_output_root, contentBase: paths.gallery_output_root,
port: 8100, port: 8100,
listenHost: "0.0.0.0", listenHost: "0.0.0.0",
}) });
);
gulp.task("rspack-prod-gallery", () => export const rspackProdGallery = () =>
prodBuild( prodBuild(
createGalleryConfig({ createGalleryConfig({
isProdBuild: true, isProdBuild: true,
latestBuild: true, latestBuild: true,
}) })
) );
);
gulp.task("rspack-watch-landing-page", () => { export const rspackWatchLandingPage = () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
rspack( rspack(
process.env.ES5 process.env.ES5
@@ -212,21 +222,17 @@ gulp.task("rspack-watch-landing-page", () => {
: createLandingPageConfig({ isProdBuild: false, latestBuild: true }) : createLandingPageConfig({ isProdBuild: false, latestBuild: true })
).watch({ poll: isWsl }, doneHandler()); ).watch({ poll: isWsl }, doneHandler());
gulp.watch( watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series( series(buildLandingPageTranslations, copyTranslationsLandingPage)
"build-landing-page-translations",
"copy-translations-landing-page"
)
); );
}); };
gulp.task("rspack-prod-landing-page", () => export const rspackProdLandingPage = () =>
prodBuild( prodBuild(
bothBuilds(createLandingPageConfig, { bothBuilds(createLandingPageConfig, {
isProdBuild: true, isProdBuild: true,
isStatsBuild: env.isStatsBuild(), isStatsBuild: isStatsBuild(),
isTestBuild: env.isTestBuild(), isTestBuild: isTestBuild(),
}) })
) );
);

View File

@@ -1,11 +1,10 @@
// Generate service workers // Generate service workers
import { deleteAsync } from "del"; import { deleteAsync } from "del";
import gulp from "gulp";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises"; import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { basename, join, relative } from "node:path"; import { basename, join, relative } from "node:path";
import { injectManifest } from "workbox-build"; import { injectManifest } from "workbox-build";
import paths from "../paths.cjs"; import paths from "../paths.ts";
const SW_MAP = { const SW_MAP = {
[paths.app_output_latest]: "modern", [paths.app_output_latest]: "modern",
@@ -23,7 +22,7 @@ self.addEventListener('install', (event) => {
}); });
`.trim() + "\n"; `.trim() + "\n";
gulp.task("gen-service-worker-app-dev", async () => { export const genServiceWorkerAppDev = async () => {
await mkdir(paths.app_output_root, { recursive: true }); await mkdir(paths.app_output_root, { recursive: true });
await Promise.all( await Promise.all(
Object.values(SW_MAP).map((build) => Object.values(SW_MAP).map((build) =>
@@ -32,9 +31,9 @@ gulp.task("gen-service-worker-app-dev", async () => {
}) })
) )
); );
}); };
gulp.task("gen-service-worker-app-prod", () => export const genServiceWorkerAppProd = () =>
Promise.all( Promise.all(
Object.entries(SW_MAP).map(async ([outPath, build]) => { Object.entries(SW_MAP).map(async ([outPath, build]) => {
const manifest = JSON.parse( const manifest = JSON.parse(
@@ -83,5 +82,4 @@ gulp.task("gen-service-worker-app-prod", () =>
await symlink(basename(swDest), swOld); await symlink(basename(swDest), swOld);
} }
}) })
) );
);

View File

@@ -2,7 +2,7 @@
import { deleteAsync } from "del"; import { deleteAsync } from "del";
import { glob } from "glob"; import { glob } from "glob";
import gulp from "gulp"; import { src as glupSrc, dest as gulpDest, parallel, series } from "gulp";
import rename from "gulp-rename"; import rename from "gulp-rename";
import merge from "lodash.merge"; import merge from "lodash.merge";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
@@ -10,9 +10,12 @@ import { mkdir, readFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { PassThrough, Transform } from "node:stream"; import { PassThrough, Transform } from "node:stream";
import { finished } from "node:stream/promises"; import { finished } from "node:stream/promises";
import env from "../env.cjs"; import { isProdBuild } from "../env.ts";
import paths from "../paths.cjs"; import paths from "../paths.ts";
import "./fetch-nightly-translations.js"; import {
allowSetupFetchNightlyTranslations,
fetchNightlyTranslations,
} from "./fetch-nightly-translations.ts";
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
@@ -23,18 +26,20 @@ const TEST_LOCALE = "en-x-test";
let mergeBackend = false; let mergeBackend = false;
gulp.task( // translations-enable-merge-backend
"translations-enable-merge-backend", export const translationsEnableMergeBackend = parallel(async () => {
gulp.parallel(async () => { mergeBackend = true;
mergeBackend = true; }, allowSetupFetchNightlyTranslations);
}, "allow-setup-fetch-nightly-translations")
);
// Transform stream to apply a function on Vinyl JSON files (buffer mode only). // Transform stream to apply a function on Vinyl JSON files (buffer mode only).
// The provided function can either return a new object, or an array of // The provided function can either return a new object, or an array of
// [object, subdirectory] pairs for fragmentizing the JSON. // [object, subdirectory] pairs for fragmentizing the JSON.
class CustomJSON extends Transform { class CustomJSON extends Transform {
constructor(func, reviver = null) { _func: any;
_reviver: any;
constructor(func, reviver: any = null) {
super({ objectMode: true }); super({ objectMode: true });
this._func = func; this._func = func;
this._reviver = reviver; this._reviver = reviver;
@@ -56,9 +61,17 @@ class CustomJSON extends Transform {
// Transform stream to merge Vinyl JSON files (buffer mode only). // Transform stream to merge Vinyl JSON files (buffer mode only).
class MergeJSON extends Transform { class MergeJSON extends Transform {
_objects = []; _objects: any[] = [];
constructor(stem, startObj = {}, reviver = null) { _stem: any;
_startObj: any;
_reviver: any;
_outFile: any;
constructor(stem, startObj = {}, reviver: any = null) {
super({ objectMode: true, allowHalfOpen: false }); super({ objectMode: true, allowHalfOpen: false });
this._stem = stem; this._stem = stem;
this._startObj = structuredClone(startObj); this._startObj = structuredClone(startObj);
@@ -111,11 +124,12 @@ const testReviver = (_key, value) =>
const KEY_REFERENCE = /\[%key:([^%]+)%\]/; const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
const lokaliseTransform = (data, path, original = data) => { const lokaliseTransform = (data, path, original = data) => {
const output = {}; const output = {};
for (const [key, value] of Object.entries(data)) { for (const entry of Object.entries(data)) {
const [key, value] = entry as [string, string];
if (typeof value === "object") { if (typeof value === "object") {
output[key] = lokaliseTransform(value, path, original); output[key] = lokaliseTransform(value, path, original);
} else { } else {
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => { output[key] = value?.replace(KEY_REFERENCE, (_match, lokalise_key) => {
const replace = lokalise_key.split("::").reduce((tr, k) => { const replace = lokalise_key.split("::").reduce((tr, k) => {
if (!tr) { if (!tr) {
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
@@ -132,18 +146,17 @@ const lokaliseTransform = (data, path, original = data) => {
return output; return output;
}; };
gulp.task("clean-translations", () => deleteAsync([workDir])); export const cleanTranslations = () => deleteAsync([workDir]);
const makeWorkDir = () => mkdir(workDir, { recursive: true }); const makeWorkDir = () => mkdir(workDir, { recursive: true });
const createTestTranslation = () => const createTestTranslation = () =>
env.isProdBuild() isProdBuild()
? Promise.resolve() ? Promise.resolve()
: gulp : glupSrc(EN_SRC)
.src(EN_SRC)
.pipe(new CustomJSON(null, testReviver)) .pipe(new CustomJSON(null, testReviver))
.pipe(rename(`${TEST_LOCALE}.json`)) .pipe(rename(`${TEST_LOCALE}.json`))
.pipe(gulp.dest(workDir)); .pipe(gulpDest(workDir));
/** /**
* This task will build a master translation file, to be used as the base for * This task will build a master translation file, to be used as the base for
@@ -155,11 +168,10 @@ const createTestTranslation = () =>
* the Lokalise update to translations/en.json will not happen immediately. * the Lokalise update to translations/en.json will not happen immediately.
*/ */
const createMasterTranslation = () => const createMasterTranslation = () =>
gulp glupSrc([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform)) .pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en")) .pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir)); .pipe(gulpDest(workDir));
const FRAGMENTS = ["base"]; const FRAGMENTS = ["base"];
@@ -186,12 +198,12 @@ const createTranslations = async () => {
// each locale, then fragmentizes and flattens the data for final output. // each locale, then fragmentizes and flattens the data for final output.
const translationFiles = await glob([ const translationFiles = await glob([
`${inFrontendDir}/!(en).json`, `${inFrontendDir}/!(en).json`,
...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]), ...(isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
]); ]);
const hashStream = new Transform({ const hashStream = new Transform({
objectMode: true, objectMode: true,
transform: async (file, _, callback) => { transform: async (file, _, callback) => {
const hash = env.isProdBuild() const hash = isProdBuild()
? createHash("md5").update(file.contents).digest("hex") ? createHash("md5").update(file.contents).digest("hex")
: "dev"; : "dev";
HASHES.set(file.stem, hash); HASHES.set(file.stem, hash);
@@ -230,7 +242,7 @@ const createTranslations = async () => {
}) })
) )
) )
.pipe(gulp.dest(outDir)); .pipe(gulpDest(outDir));
// Send the English master downstream first, then for each other locale // Send the English master downstream first, then for each other locale
// generate merged JSON data to continue piping. It begins with the master // generate merged JSON data to continue piping. It begins with the master
@@ -240,15 +252,15 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved. // TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated // Will be OK for now as long as we don't have anything more complicated
// than a base translation + region. // than a base translation + region.
const masterStream = gulp const masterStream = glupSrc(`${workDir}/en.json`).pipe(
.src(`${workDir}/en.json`) new PassThrough({ objectMode: true })
.pipe(new PassThrough({ objectMode: true })); );
masterStream.pipe(hashStream, { end: false }); masterStream.pipe(hashStream, { end: false });
const mergesFinished = [finished(masterStream)]; const mergesFinished = [finished(masterStream)];
for (const translationFile of translationFiles) { for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json"); const locale = basename(translationFile, ".json");
const subtags = locale.split("-"); const subtags = locale.split("-");
const mergeFiles = []; const mergeFiles: string[] = [];
for (let i = 1; i <= subtags.length; i++) { for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-"); const lang = subtags.slice(0, i).join("-");
if (lang === TEST_LOCALE) { if (lang === TEST_LOCALE) {
@@ -260,9 +272,9 @@ const createTranslations = async () => {
} }
} }
} }
const mergeStream = gulp const mergeStream = glupSrc(mergeFiles, { allowEmpty: true }).pipe(
.src(mergeFiles, { allowEmpty: true }) new MergeJSON(locale, enMaster, emptyReviver)
.pipe(new MergeJSON(locale, enMaster, emptyReviver)); );
mergesFinished.push(finished(mergeStream)); mergesFinished.push(finished(mergeStream));
mergeStream.pipe(hashStream, { end: false }); mergeStream.pipe(hashStream, { end: false });
} }
@@ -275,12 +287,11 @@ const createTranslations = async () => {
}; };
const writeTranslationMetaData = () => const writeTranslationMetaData = () =>
gulp glupSrc([`${paths.translations_src}/translationMetadata.json`])
.src([`${paths.translations_src}/translationMetadata.json`])
.pipe( .pipe(
new CustomJSON((meta) => { new CustomJSON((meta) => {
// Add the test translation in development. // Add the test translation in development.
if (!env.isProdBuild()) { if (!isProdBuild()) {
meta[TEST_LOCALE] = { nativeName: "Translation Test" }; meta[TEST_LOCALE] = { nativeName: "Translation Test" };
} }
// Filter out locales without a native name, and add the hashes. // Filter out locales without a native name, and add the hashes.
@@ -300,28 +311,22 @@ const writeTranslationMetaData = () =>
}; };
}) })
) )
.pipe(gulp.dest(workDir)); .pipe(gulpDest(workDir));
gulp.task( export const buildTranslations = series(
"build-translations", parallel(fetchNightlyTranslations, series(cleanTranslations, makeWorkDir)),
gulp.series( createTestTranslation,
gulp.parallel( createMasterTranslation,
"fetch-nightly-translations", createTranslations,
gulp.series("clean-translations", makeWorkDir) writeTranslationMetaData
),
createTestTranslation,
createMasterTranslation,
createTranslations,
writeTranslationMetaData
)
); );
gulp.task( export const buildSupervisorTranslations = series(
"build-supervisor-translations", setFragment("supervisor"),
gulp.series(setFragment("supervisor"), "build-translations") buildTranslations
); );
gulp.task( export const buildLandingPageTranslations = series(
"build-landing-page-translations", setFragment("landing-page"),
gulp.series(setFragment("landing-page"), "build-translations") buildTranslations
); );

View File

@@ -5,10 +5,11 @@ import { version as babelVersion } from "@babel/core";
import presetEnv from "@babel/preset-env"; import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets"; import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat"; import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js"; import { logPlugin } from "@babel/preset-env/lib/debug.js";
// eslint-disable-next-line import/no-relative-packages // eslint-disable-next-line import/no-relative-packages
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js"; import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
import { babelOptions } from "./bundle.cjs"; import { babelOptions } from "./bundle.ts";
const detailsOpen = (heading) => const detailsOpen = (heading) =>
`<details>\n<summary><h4>${heading}</h4></summary>\n`; `<details>\n<summary><h4>${heading}</h4></summary>\n`;
@@ -50,6 +51,12 @@ for (const buildType of ["Modern", "Legacy"]) {
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
const presetEnvOpts = babelOpts.presets[0][1]; const presetEnvOpts = babelOpts.presets[0][1];
if (typeof presetEnvOpts !== "object") {
throw new Error(
"The first preset in babelOptions is not an object. This is unexpected."
);
}
// Invoking preset-env in debug mode will log the included plugins // Invoking preset-env in debug mode will log the included plugins
console.log(detailsOpen(`${buildType} Build Babel Plugins`)); console.log(detailsOpen(`${buildType} Build Babel Plugins`));
presetEnv.default(dummyAPI, { presetEnv.default(dummyAPI, {

View File

@@ -1,63 +0,0 @@
const path = require("path");
module.exports = {
root_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
app_output_root: path.resolve(__dirname, "../hass_frontend"),
app_output_static: path.resolve(__dirname, "../hass_frontend/static"),
app_output_latest: path.resolve(
__dirname,
"../hass_frontend/frontend_latest"
),
app_output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
demo_dir: path.resolve(__dirname, "../demo"),
demo_output_root: path.resolve(__dirname, "../demo/dist"),
demo_output_static: path.resolve(__dirname, "../demo/dist/static"),
demo_output_latest: path.resolve(__dirname, "../demo/dist/frontend_latest"),
demo_output_es5: path.resolve(__dirname, "../demo/dist/frontend_es5"),
cast_dir: path.resolve(__dirname, "../cast"),
cast_output_root: path.resolve(__dirname, "../cast/dist"),
cast_output_static: path.resolve(__dirname, "../cast/dist/static"),
cast_output_latest: path.resolve(__dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
gallery_dir: path.resolve(__dirname, "../gallery"),
gallery_build: path.resolve(__dirname, "../gallery/build"),
gallery_output_root: path.resolve(__dirname, "../gallery/dist"),
gallery_output_latest: path.resolve(
__dirname,
"../gallery/dist/frontend_latest"
),
gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"),
landingPage_dir: path.resolve(__dirname, "../landing-page"),
landingPage_build: path.resolve(__dirname, "../landing-page/build"),
landingPage_output_root: path.resolve(__dirname, "../landing-page/dist"),
landingPage_output_latest: path.resolve(
__dirname,
"../landing-page/dist/frontend_latest"
),
landingPage_output_es5: path.resolve(
__dirname,
"../landing-page/dist/frontend_es5"
),
landingPage_output_static: path.resolve(
__dirname,
"../landing-page/dist/static"
),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"),
};

63
build-scripts/paths.ts Normal file
View File

@@ -0,0 +1,63 @@
import path, { dirname as pathDirname } from "node:path";
import { fileURLToPath } from "node:url";
export const dirname = pathDirname(fileURLToPath(import.meta.url));
export default {
root_dir: path.resolve(dirname, ".."),
build_dir: path.resolve(dirname, "../build"),
app_output_root: path.resolve(dirname, "../hass_frontend"),
app_output_static: path.resolve(dirname, "../hass_frontend/static"),
app_output_latest: path.resolve(dirname, "../hass_frontend/frontend_latest"),
app_output_es5: path.resolve(dirname, "../hass_frontend/frontend_es5"),
demo_dir: path.resolve(dirname, "../demo"),
demo_output_root: path.resolve(dirname, "../demo/dist"),
demo_output_static: path.resolve(dirname, "../demo/dist/static"),
demo_output_latest: path.resolve(dirname, "../demo/dist/frontend_latest"),
demo_output_es5: path.resolve(dirname, "../demo/dist/frontend_es5"),
cast_dir: path.resolve(dirname, "../cast"),
cast_output_root: path.resolve(dirname, "../cast/dist"),
cast_output_static: path.resolve(dirname, "../cast/dist/static"),
cast_output_latest: path.resolve(dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(dirname, "../cast/dist/frontend_es5"),
gallery_dir: path.resolve(dirname, "../gallery"),
gallery_build: path.resolve(dirname, "../gallery/build"),
gallery_output_root: path.resolve(dirname, "../gallery/dist"),
gallery_output_latest: path.resolve(
dirname,
"../gallery/dist/frontend_latest"
),
gallery_output_static: path.resolve(dirname, "../gallery/dist/static"),
landingPage_dir: path.resolve(dirname, "../landing-page"),
landingPage_build: path.resolve(dirname, "../landing-page/build"),
landingPage_output_root: path.resolve(dirname, "../landing-page/dist"),
landingPage_output_latest: path.resolve(
dirname,
"../landing-page/dist/frontend_latest"
),
landingPage_output_es5: path.resolve(
dirname,
"../landing-page/dist/frontend_es5"
),
landingPage_output_static: path.resolve(
dirname,
"../landing-page/dist/static"
),
hassio_dir: path.resolve(dirname, "../hassio"),
hassio_output_root: path.resolve(dirname, "../hassio/build"),
hassio_output_static: path.resolve(dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(dirname, "../src/translations"),
};

View File

@@ -1,20 +1,25 @@
const { existsSync } = require("fs"); import filterStats from "@bundle-stats/plugin-webpack-filter";
const path = require("path"); import { RsdoctorRspackPlugin } from "@rsdoctor/rspack-plugin";
const rspack = require("@rspack/core"); import { DefinePlugin, NormalModuleReplacementPlugin } from "@rspack/core";
// eslint-disable-next-line @typescript-eslint/naming-convention import { defineConfig } from "@rspack/cli";
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin"); import log from "fancy-log";
// eslint-disable-next-line @typescript-eslint/naming-convention import { existsSync } from "node:fs";
const { StatsWriterPlugin } = require("webpack-stats-plugin"); import path from "node:path";
const filterStats = require("@bundle-stats/plugin-webpack-filter"); import { WebpackManifestPlugin } from "rspack-manifest-plugin";
// eslint-disable-next-line @typescript-eslint/naming-convention import TerserPlugin from "terser-webpack-plugin";
const TerserPlugin = require("terser-webpack-plugin"); import { StatsWriterPlugin } from "webpack-stats-plugin";
// eslint-disable-next-line @typescript-eslint/naming-convention // @ts-ignore
const { WebpackManifestPlugin } = require("rspack-manifest-plugin"); import WebpackBar from "webpackbar/rspack";
const log = require("fancy-log"); import {
// eslint-disable-next-line @typescript-eslint/naming-convention babelOptions,
const WebpackBar = require("webpackbar/rspack"); config,
const paths = require("./paths.cjs"); definedVars,
const bundle = require("./bundle.cjs"); emptyPackages,
sourceMapURL,
swcOptions,
terserOptions,
} from "./bundle.ts";
import paths from "./paths.ts";
class LogStartCompilePlugin { class LogStartCompilePlugin {
ignoredFirst = false; ignoredFirst = false;
@@ -30,7 +35,7 @@ class LogStartCompilePlugin {
} }
} }
const createRspackConfig = ({ export const createRspackConfig = ({
name, name,
entry, entry,
outputPath, outputPath,
@@ -42,12 +47,23 @@ const createRspackConfig = ({
isTestBuild, isTestBuild,
isHassioBuild, isHassioBuild,
dontHash, dontHash,
}: {
name: string;
entry: any;
outputPath: string;
publicPath: string;
defineOverlay?: Record<string, any>;
isProdBuild?: boolean;
latestBuild?: boolean;
isStatsBuild?: boolean;
isTestBuild?: boolean;
isHassioBuild?: boolean;
dontHash?: Set<string>;
}) => { }) => {
if (!dontHash) { if (!dontHash) {
dontHash = new Set(); dontHash = new Set();
} }
const ignorePackages = bundle.ignorePackages({ latestBuild }); return defineConfig({
return {
name, name,
mode: isProdBuild ? "production" : "development", mode: isProdBuild ? "production" : "development",
target: `browserslist:${latestBuild ? "modern" : "legacy"}`, target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
@@ -70,7 +86,7 @@ const createRspackConfig = ({
{ {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
...bundle.babelOptions({ ...babelOptions({
latestBuild, latestBuild,
isProdBuild, isProdBuild,
isTestBuild, isTestBuild,
@@ -82,7 +98,7 @@ const createRspackConfig = ({
}, },
{ {
loader: "builtin:swc-loader", loader: "builtin:swc-loader",
options: bundle.swcOptions(), options: swcOptions(),
}, },
], ],
resolve: { resolve: {
@@ -103,7 +119,7 @@ const createRspackConfig = ({
new TerserPlugin({ new TerserPlugin({
parallel: true, parallel: true,
extractComments: true, extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }), terserOptions: terserOptions({ latestBuild, isTestBuild }),
}), }),
], ],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
@@ -122,7 +138,7 @@ const createRspackConfig = ({
!chunk.canBeInitial() && !chunk.canBeInitial() &&
!new RegExp( !new RegExp(
`^.+-work${latestBuild ? "(?:let|er)" : "let"}$` `^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
).test(chunk.name), ).test(chunk?.name || ""),
}, },
}, },
plugins: [ plugins: [
@@ -131,44 +147,11 @@ const createRspackConfig = ({
// Only include the JS of entrypoints // Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"), filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}), }),
new rspack.DefinePlugin( new DefinePlugin(
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) definedVars({ isProdBuild, latestBuild, defineOverlay })
), ),
new rspack.IgnorePlugin({ new NormalModuleReplacementPlugin(
checkResource(resource, context) { new RegExp(emptyPackages({ isHassioBuild }).join("|")),
// Only use ignore to intercept imports that we don't control
// inside node_module dependencies.
if (
!context.includes("/node_modules/") ||
// calling define.amd will call require("!!webpack amd options")
resource.startsWith("!!webpack") ||
// loaded by webpack dev server but doesn't exist.
resource === "webpack/hot" ||
resource.startsWith("@swc/helpers")
) {
return false;
}
let fullPath;
try {
fullPath = resource.startsWith(".")
? path.resolve(context, resource)
: require.resolve(resource);
} catch (err) {
console.error(
"Error in Home Assistant ignore plugin",
resource,
context
);
throw err;
}
return ignorePackages.some((toIgnorePath) =>
fullPath.startsWith(toIgnorePath)
);
},
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js") path.resolve(paths.root_dir, "src/util/empty.js")
), ),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),
@@ -184,7 +167,9 @@ const createRspackConfig = ({
isProdBuild && isProdBuild &&
isStatsBuild && isStatsBuild &&
new RsdoctorRspackPlugin({ new RsdoctorRspackPlugin({
reportDir: path.join(paths.build_dir, "rsdoctor"), output: {
reportDir: path.join(paths.build_dir, "rsdoctor"),
},
features: ["plugins", "bundle"], features: ["plugins", "bundle"],
supports: { supports: {
generateTileGraph: true, generateTileGraph: true,
@@ -219,7 +204,9 @@ const createRspackConfig = ({
output: { output: {
module: latestBuild, module: latestBuild,
filename: ({ chunk }) => filename: ({ chunk }) =>
!isProdBuild || isStatsBuild || dontHash.has(chunk.name) !isProdBuild ||
isStatsBuild ||
(chunk?.name && dontHash.has(chunk.name))
? "[name].js" ? "[name].js"
: "[name].[contenthash].js", : "[name].[contenthash].js",
chunkFilename: chunkFilename:
@@ -250,7 +237,7 @@ const createRspackConfig = ({
// dev tools, and they stay happy getting 404s with valid requests. // dev tools, and they stay happy getting 404s with valid requests.
return `/unknown${path.resolve("/", info.resourcePath)}`; return `/unknown${path.resolve("/", info.resourcePath)}`;
} }
return new URL(info.resourcePath, bundle.sourceMapURL()).href; return new URL(info.resourcePath, sourceMapURL()).href;
} }
: undefined, : undefined,
]) ])
@@ -260,35 +247,51 @@ const createRspackConfig = ({
layers: true, layers: true,
outputModule: true, outputModule: true,
}, },
}; });
}; };
const createAppConfig = ({ export const createAppConfig = ({
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
isTestBuild, isTestBuild,
}: {
isProdBuild?: boolean;
latestBuild?: boolean;
isStatsBuild?: boolean;
isTestBuild?: boolean;
}) => }) =>
createRspackConfig( createRspackConfig(
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
); );
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => export const createDemoConfig = ({
createRspackConfig( isProdBuild,
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild }) latestBuild,
); isStatsBuild,
}: {
isProdBuild?: boolean;
latestBuild?: boolean;
isStatsBuild?: boolean;
}) =>
createRspackConfig(config.demo({ isProdBuild, latestBuild, isStatsBuild }));
const createCastConfig = ({ isProdBuild, latestBuild }) => export const createCastConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild })); createRspackConfig(config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({ export const createHassioConfig = ({
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
isTestBuild, isTestBuild,
}: {
isProdBuild?: boolean;
latestBuild?: boolean;
isStatsBuild?: boolean;
isTestBuild?: boolean;
}) => }) =>
createRspackConfig( createRspackConfig(
bundle.config.hassio({ config.hassio({
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
@@ -296,18 +299,8 @@ const createHassioConfig = ({
}) })
); );
const createGalleryConfig = ({ isProdBuild, latestBuild }) => export const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); createRspackConfig(config.gallery({ isProdBuild, latestBuild }));
const createLandingPageConfig = ({ isProdBuild, latestBuild }) => export const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild })); createRspackConfig(config.landingPage({ isProdBuild, latestBuild }));
module.exports = {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,
};

42
build-scripts/runTask.ts Normal file
View File

@@ -0,0 +1,42 @@
// run-build.ts
import { series } from "gulp";
import { availableParallelism } from "node:os";
import tasks from "./gulp/index.ts";
process.env.UV_THREADPOOL_SIZE = availableParallelism().toString();
const runGulpTask = async (runTasks: string[]) => {
try {
for (const taskName of runTasks) {
if (tasks[taskName] === undefined) {
console.error(`Gulp task "${taskName}" does not exist.`);
console.log("Available tasks:");
Object.keys(tasks).forEach((task) => {
console.log(` - ${task}`);
});
process.exit(1);
}
}
await new Promise((resolve, reject) => {
series(...runTasks.map((taskName) => tasks[taskName]))((err?: Error) => {
if (err) {
reject(err);
} else {
resolve(null);
}
});
});
process.exit(0);
} catch (error: any) {
console.error(`Error running Gulp task "${runTasks}":`, error);
process.exit(1);
}
};
// Get the task name from command line arguments
// TODO arg validation
const tasksToRun = process.argv.slice(2);
runGulpTask(tasksToRun);

View File

@@ -14,5 +14,5 @@
"name": "Home Assistant Cast", "name": "Home Assistant Cast",
"short_name": "HA Cast", "short_name": "HA Cast",
"start_url": "/?homescreen=1", "start_url": "/?homescreen=1",
"theme_color": "#009ac7" "theme_color": "#03A9F4"
} }

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-cast yarn run-task build-cast

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-cast yarn run-task develop-cast

View File

@@ -1,3 +1,5 @@
import "@material/mwc-button/mwc-button";
import type { ActionDetail } from "@material/mwc-list/mwc-list"; import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import type { Auth, Connection } from "home-assistant-js-websocket"; import type { Auth, Connection } from "home-assistant-js-websocket";
@@ -18,7 +20,6 @@ import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-list"; import "../../../../src/components/ha-list";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-list-item"; import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { import {
@@ -62,20 +63,12 @@ class HcCast extends LitElement {
<p class="question action-item"> <p class="question action-item">
Stay logged in? Stay logged in?
<span> <span>
<ha-button <mwc-button @click=${this._handleSaveTokens}>
appearance="plain"
size="small"
@click=${this._handleSaveTokens}
>
YES YES
</ha-button> </mwc-button>
<ha-button <mwc-button @click=${this._handleSkipSaveTokens}>
appearance="plain"
size="small"
@click=${this._handleSkipSaveTokens}
>
NO NO
</ha-button> </mwc-button>
</span> </span>
</p> </p>
` `
@@ -85,10 +78,10 @@ class HcCast extends LitElement {
: !this.castManager.status : !this.castManager.status
? html` ? html`
<p class="center-item"> <p class="center-item">
<ha-button @click=${this._handleLaunch}> <mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon> <ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting Start Casting
</ha-button> </mwc-button>
</p> </p>
` `
: html` : html`
@@ -128,22 +121,14 @@ class HcCast extends LitElement {
<div class="card-actions"> <div class="card-actions">
${this.castManager.status ${this.castManager.status
? html` ? html`
<ha-button appearance="plain" @click=${this._handleLaunch}> <mwc-button @click=${this._handleLaunch}>
<ha-svg-icon <ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
slot="start"
.path=${mdiCastConnected}
></ha-svg-icon>
Manage Manage
</ha-button> </mwc-button>
` `
: ""} : ""}
<div class="spacer"></div> <div class="spacer"></div>
<ha-button <mwc-button @click=${this._handleLogout}>Log out</mwc-button>
variant="danger"
appearance="plain"
@click=${this._handleLogout}
>Log out</ha-button
>
</div> </div>
</hc-layout> </hc-layout>
`; `;
@@ -260,6 +245,13 @@ class HcCast extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
mwc-button ha-svg-icon {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
height: 18px;
}
ha-list-item ha-icon, ha-list-item ha-icon,
ha-list-item ha-svg-icon { ha-list-item ha-svg-icon {
padding: 12px; padding: 12px;

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js"; import { mdiCastConnected, mdiCast } from "@mdi/js";
import type { import type {
Auth, Auth,
@@ -27,7 +28,6 @@ import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker"; import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout"; import "./hc-layout";
import "../../../../src/components/ha-textfield"; import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-button";
const seeFAQ = (qid) => html` const seeFAQ = (qid) => html`
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
@@ -83,14 +83,11 @@ export class HcConnect extends LitElement {
Unable to connect to ${tokens!.hassUrl}. Unable to connect to ${tokens!.hassUrl}.
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-button appearance="plain" href="/">Retry</ha-button> <a href="/">
<mwc-button> Retry </mwc-button>
</a>
<div class="spacer"></div> <div class="spacer"></div>
<ha-button <mwc-button @click=${this._handleLogout}>Log out</mwc-button>
appearance="plain"
variant="danger"
@click=${this._handleLogout}
>Log out</ha-button
>
</div> </div>
</hc-layout> </hc-layout>
`; `;
@@ -131,19 +128,16 @@ export class HcConnect extends LitElement {
${this.error ? html` <p class="error">${this.error}</p> ` : ""} ${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-button appearance="plain" @click=${this._handleDemo}> <mwc-button @click=${this._handleDemo}>
Show Demo Show Demo
<ha-svg-icon <ha-svg-icon
slot="end"
.path=${this.castManager.castState === "CONNECTED" .path=${this.castManager.castState === "CONNECTED"
? mdiCastConnected ? mdiCastConnected
: mdiCast} : mdiCast}
></ha-svg-icon> ></ha-svg-icon>
</ha-button> </mwc-button>
<div class="spacer"></div> <div class="spacer"></div>
<ha-button appearance="plain" @click=${this._handleConnect} <mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
>Authorize</ha-button
>
</div> </div>
</hc-layout> </hc-layout>
`; `;
@@ -315,6 +309,10 @@ export class HcConnect extends LitElement {
color: darkred; color: darkred;
} }
mwc-button ha-svg-icon {
margin-left: 8px;
}
.spacer { .spacer {
flex: 1; flex: 1;
} }

View File

@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager(); const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor( playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD, framework.messages.MessageType.LOAD,
(loadRequestData) => { (loadRequestData) => {
const media = loadRequestData.media; const media = loadRequestData.media;
// Special handling if it came from Google Assistant // Special handling if it came from Google Assistant
if (media.entity) { if (media.entity) {
media.contentId = media.entity; media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE; media.streamType = framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl"; media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore // @ts-ignore
media.hlsVideoSegmentFormat = media.hlsVideoSegmentFormat =
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; framework.messages.HlsVideoSegmentFormat.FMP4;
} }
return loadRequestData; return loadRequestData;
} }

View File

@@ -40,8 +40,7 @@ const playDummyMedia = (viewTitle?: string) => {
loadRequestData.media.contentId = loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png"; "https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg"; loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = loadRequestData.media.streamType = framework.messages.StreamType.NONE;
"NONE" as framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata(); const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle; metadata.title = viewTitle;
loadRequestData.media.metadata = metadata; loadRequestData.media.metadata = metadata;
@@ -90,7 +89,7 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions(); const options = new framework.CastReceiverOptions();
options.disableIdleTimeout = true; options.disableIdleTimeout = true;
options.customNamespaces = { options.customNamespaces = {
[CAST_NS]: "json" as framework.system.MessageType.JSON, [CAST_NS]: framework.system.MessageType.JSON,
}; };
castContext.addCustomMessageListener( castContext.addCustomMessageListener(
@@ -98,7 +97,9 @@ castContext.addCustomMessageListener(
// @ts-ignore // @ts-ignore
(ev: ReceivedMessage<HassMessage>) => { (ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller // We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (playerManager.getPlayerState() !== "IDLE") { if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
playerManager.stop(); playerManager.stop();
} else { } else {
showLovelaceController(); showLovelaceController();
@@ -112,7 +113,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager(); const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor( playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD, framework.messages.MessageType.LOAD,
(loadRequestData) => { (loadRequestData) => {
if ( if (
loadRequestData.media.contentId === loadRequestData.media.contentId ===
@@ -126,23 +127,24 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant // Special handling if it came from Google Assistant
if (media.entity) { if (media.entity) {
media.contentId = media.entity; media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE; media.streamType = framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl"; media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore // @ts-ignore
media.hlsVideoSegmentFormat = media.hlsVideoSegmentFormat =
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; framework.messages.HlsVideoSegmentFormat.FMP4;
} }
return loadRequestData; return loadRequestData;
} }
); );
playerManager.addEventListener( playerManager.addEventListener(
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS, framework.events.EventType.MEDIA_STATUS,
(event) => { (event) => {
if ( if (
event.mediaStatus?.playerState === "IDLE" && event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.idleReason && event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !== "INTERRUPTED" event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
) { ) {
// media finished or stopped, return to default Lovelace // media finished or stopped, return to default Lovelace
showLovelaceController(); showLovelaceController();

View File

@@ -75,5 +75,5 @@
"name": "Home Assistant Demo", "name": "Home Assistant Demo",
"short_name": "HA Demo", "short_name": "HA Demo",
"start_url": "/?homescreen=1", "start_url": "/?homescreen=1",
"theme_color": "#009ac7" "theme_color": "#03A9F4"
} }

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-demo yarn run-task build-demo

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-demo yarn run-task develop-demo

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp analyze-demo yarn run-task analyze-demo

View File

@@ -89,14 +89,11 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)} )}
</div> </div>
<div class="actions small-hidden"> <div class="actions small-hidden">
<ha-button <a href="https://www.home-assistant.io" target="_blank">
appearance="plain" <ha-button>
size="small" ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
href="https://www.home-assistant.io" </ha-button>
target="_blank" </a>
>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
</div> </div>
</ha-card> </ha-card>
`; `;

View File

@@ -68,7 +68,7 @@
} }
#ha-launch-screen .ha-launch-screen-spacer-top { #ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1; flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px ); margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px; padding-top: 48px;
} }
#ha-launch-screen .ha-launch-screen-spacer-bottom { #ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px; padding-top: 48px;
} }
.ohf-logo { .ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0; margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@@ -56,15 +56,6 @@ export default tseslint.config(
}, },
}, },
}, },
settings: {
"import/resolver": {
webpack: {
config: "./rspack.config.cjs",
},
},
},
rules: { rules: {
"class-methods-use-this": "off", "class-methods-use-this": "off",
"new-cap": "off", "new-cap": "off",
@@ -187,5 +178,12 @@ export default tseslint.config(
], ],
"no-use-before-define": "off", "no-use-before-define": "off",
}, },
settings: {
"import/resolver": {
node: {
extensions: [".ts", ".js"],
},
},
},
} }
); );

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-gallery yarn run-task build-gallery

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-gallery yarn run-task develop-gallery

View File

@@ -1,11 +1,11 @@
import "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { html, LitElement, css, nothing } from "lit"; import { html, LitElement, css, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import type { HaButton } from "../../../src/components/ha-button";
@customElement("demo-black-white-row") @customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement { class DemoBlackWhiteRow extends LitElement {
@@ -25,9 +25,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="light"></slot> <slot name="light"></slot>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}> <mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit Submit
</ha-button> </mwc-button>
</div> </div>
</ha-card> </ha-card>
</div> </div>
@@ -37,9 +40,12 @@ class DemoBlackWhiteRow extends LitElement {
<slot name="dark"></slot> <slot name="dark"></slot>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}> <mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit Submit
</ha-button> </mwc-button>
</div> </div>
</ha-card> </ha-card>
${this.value ${this.value
@@ -68,7 +74,7 @@ class DemoBlackWhiteRow extends LitElement {
} }
handleSubmit(ev) { handleSubmit(ev) {
const content = (ev.target as HaButton).closest(".content")!; const content = (ev.target as Button).closest(".content")!;
fireEvent(this, "submitted" as any, { fireEvent(this, "submitted" as any, {
slot: content.classList.contains("light") ? "light" : "dark", slot: content.classList.contains("light") ? "light" : "dark",
}); });

View File

@@ -1,11 +1,10 @@
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content"; import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content"; import "../../../src/state-summary/state-card-content";
import "../ha-demo-options"; import "../ha-demo-options";
import type { HomeAssistant } from "../../../src/types"; import type { HomeAssistant } from "../../../src/types";
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
@customElement("demo-more-info") @customElement("demo-more-info")
class DemoMoreInfo extends LitElement { class DemoMoreInfo extends LitElement {
@@ -22,13 +21,11 @@ class DemoMoreInfo extends LitElement {
<div class="root"> <div class="root">
<div id="card"> <div id="card">
<ha-card> <ha-card>
${!computeShowNewMoreInfo(state) <state-card-content
? html`<state-card-content .stateObj=${state}
.stateObj=${state} .hass=${this.hass}
.hass=${this.hass} in-dialog
in-dialog ></state-card-content>
></state-card-content>`
: nothing}
<more-info-content <more-info-content
.hass=${this.hass} .hass=${this.hass}

View File

@@ -1106,7 +1106,7 @@ export default {
friendly_name: "Philips Hue", friendly_name: "Philips Hue",
entity_picture: null, entity_picture: null,
description: description:
"Press the button on the bridge to register Philips Hue with Home Assistant.", "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Description image](/static/images/config_philips_hue.jpg)",
submit_caption: "I have pressed the button", submit_caption: "I have pressed the button",
}, },
last_changed: "2018-07-19T10:44:46.515160+00:00", last_changed: "2018-07-19T10:44:46.515160+00:00",

View File

@@ -18,6 +18,7 @@ import { HaDeviceAction } from "../../../../src/panels/config/automation/action/
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence"; import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
@@ -31,6 +32,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Service", actions: [HaServiceAction.defaultConfig] }, { name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] }, { name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] }, { name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] }, { name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, { name: "Repeat", actions: [HaRepeatAction.defaultConfig] },

View File

@@ -147,13 +147,13 @@ The `title ` option should not be used without a description.
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is a success alert — check it out! This is a success alert — check it out!
<ha-button slot="action">Undo</ha-button> <mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert> </ha-alert>
```html ```html
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is a success alert — check it out! This is a success alert — check it out!
<ha-button slot="action">Undo</ha-button> <mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert> </ha-alert>
``` ```

View File

@@ -1,10 +1,10 @@
import "@material/mwc-button/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-logo-svg"; import "../../../../src/components/ha-logo-svg";
const alerts: { const alerts: {
@@ -78,13 +78,13 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action", description: "This is a test error alert with action",
type: "error", type: "error",
actionSlot: html`<ha-button size="small" slot="action">restart</ha-button>`, actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
}, },
{ {
title: "Unsaved data", title: "Unsaved data",
description: "You have unsaved data", description: "You have unsaved data",
type: "warning", type: "warning",
actionSlot: html`<ha-button size="small" slot="action">save</ha-button>`, actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`,
}, },
{ {
title: "Slotted icon", title: "Slotted icon",
@@ -108,7 +108,7 @@ const alerts: {
title: "Slotted action", title: "Slotted action",
description: "Alert with slotted action", description: "Alert with slotted action",
type: "info", type: "info",
actionSlot: html`<ha-button slot="action">action</ha-button>`, actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
}, },
{ {
description: "Dismissable information (RTL)", description: "Dismissable information (RTL)",
@@ -120,7 +120,7 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action (RTL)", description: "This is a test error alert with action (RTL)",
type: "error", type: "error",
actionSlot: html`<ha-button slot="action">restart</ha-button>`, actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
rtl: true, rtl: true,
}, },
{ {
@@ -211,7 +211,7 @@ export class DemoHaAlert extends LitElement {
max-height: 24px; max-height: 24px;
width: 24px; width: 24px;
} }
ha-button { mwc-button {
--mdc-theme-primary: var(--primary-text-color); --mdc-theme-primary: var(--primary-text-color);
} }
`; `;

View File

@@ -1,67 +0,0 @@
---
title: Button
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Button `<ha-button>`
## Implementation
### Example Usage
<div class="wrapper">
<ha-button>
simple button
</ha-button>
<ha-button appearance="plain">
plain button
</ha-button>
<ha-button appearance="filled">
filled button
</ha-button>
<ha-button size="small">
small
</ha-button>
</div>
```html
<ha-button> simple button </ha-button>
<ha-button size="small"> small </ha-button>
```
### API
This component is based on the webawesome button component.
Check the [webawesome documentation](https://webawesome.com/docs/components/button/) for more details.
**Slots**
- default slot: Label of the button
` - no default
- `start`: The prefix container (usually for icons).
` - no default
- `end`: The suffix container (usually for icons).
` - no default
**Properties/Attributes**
| Name | Type | Default | Description |
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| size | "small"/"medium" | "medium" | Sets the button size. |
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
| disabled | Boolean | false | Disables the button and prevents user interaction. |
**CSS Custom Properties**
- `--ha-button-height` - Height of the button.
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--ha-border-radius-pill)`.

View File

@@ -1,171 +0,0 @@
import { mdiHome } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import { titleCase } from "../../../../src/common/string/title-case";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
const appearances = ["accent", "filled", "plain"];
const variants = ["brand", "danger", "neutral", "warning", "success"];
@customElement("demo-components-ha-button")
export class DemoHaButton extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-button in ${mode}">
<div class="card-content">
${variants.map(
(variant) => html`
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
>
<ha-svg-icon
.path=${mdiHomeAssistant}
slot="start"
></ha-svg-icon>
${titleCase(`${variant} ${appearance}`)}
<ha-svg-icon
.path=${mdiHome}
slot="end"
></ha-svg-icon>
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
size="small"
>
${titleCase(`${variant} ${appearance}`)}
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.appearance=${appearance}
.variant=${variant}
loading
>
<ha-svg-icon
.path=${mdiHomeAssistant}
slot="start"
></ha-svg-icon>
${titleCase(`${variant} ${appearance}`)}
<ha-svg-icon
.path=${mdiHome}
slot="end"
></ha-svg-icon>
</ha-button>
`
)}
</div>
`
)}
${variants.map(
(variant) => html`
<div>
${appearances.map(
(appearance) => html`
<ha-button
.variant=${variant}
.appearance=${appearance}
disabled
>
${titleCase(`${appearance}`)}
</ha-button>
`
)}
</div>
<div>
${appearances.map(
(appearance) => html`
<ha-button
.variant=${variant}
.appearance=${appearance}
size="small"
disabled
>
${titleCase(`${appearance}`)}
</ha-button>
`
)}
</div>
`
)}
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.card-content div {
display: flex;
gap: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-button": DemoHaButton;
}
}

View File

@@ -1,4 +1,5 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";

View File

@@ -1,32 +0,0 @@
---
title: Progress Button
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Progress Button `<ha-progress-button>`
### API
This component is a wrapper around `<ha-button>` that adds support for showing progress
**Slots**
- default slot: Label of the button
` - no default
**Properties/Attributes**
| Name | Type | Default | Description |
| ---------- | ---------------------------------------------- | --------- | -------------------------------------------------- |
| label | string | "accent" | Sets the button label. |
| disabled | Boolean | false | Disables the button if true. |
| progress | Boolean | false | Shows a progress indicator on the button. |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| iconPath | string | undefined | Sets the icon path for the button. |

View File

@@ -1,139 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
@customElement("demo-components-ha-progress-button")
export class DemoHaProgressButton extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-progress-button in ${mode}">
<div class="card-content">
<ha-progress-button @click=${this._clickedSuccess}>
Success
</ha-progress-button>
<ha-progress-button @click=${this._clickedFail}>
Fail
</ha-progress-button>
<ha-progress-button size="small" @click=${this._clickedSuccess}>
small
</ha-progress-button>
<ha-progress-button
appearance="filled"
@click=${this._clickedSuccess}
>
filled
</ha-progress-button>
<ha-progress-button
appearance="plain"
@click=${this._clickedSuccess}
>
plain
</ha-progress-button>
<ha-progress-button
variant="warning"
@click=${this._clickedSuccess}
>
warning
</ha-progress-button>
<ha-progress-button
variant="neutral"
@click=${this._clickedSuccess}
label="with icon"
.iconPath=${mdiHomeAssistant}
>
With Icon
</ha-progress-button>
<ha-progress-button progress @click=${this._clickedSuccess}>
progress
</ha-progress-button>
<ha-progress-button disabled @click=${this._clickedSuccess}>
disabled
</ha-progress-button>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
private async _clickedSuccess(ev: CustomEvent): Promise<void> {
console.log("Clicked success");
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionSuccess();
button.progress = false;
}, 1000);
}
private async _clickedFail(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionError();
button.progress = false;
}, 1000);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.card-content div {
display: flex;
gap: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-progress-button": DemoHaProgressButton;
}
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";

View File

@@ -1,36 +0,0 @@
---
title: Slider
subtitle: A slider component for selecting a value from a range.
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Slider `<ha-slider>`
## Implementation
### Example Usage
<div class="wrapper">
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
</div>
```html
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
<ha-slider size="medium"></ha-slider>
```
### API
This component is based on the webawesome slider component.
Check the [webawesome documentation](https://webawesome.com/docs/components/slider/) for more details.
**CSS Custom Properties**
- `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`.

View File

@@ -1,100 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-slider";
import type { HomeAssistant } from "../../../../src/types";
@customElement("demo-components-ha-slider")
export class DemoHaSlider extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-slider ${mode} demo">
<div class="card-content">
<span>Default (disabled)</span>
<ha-slider
disabled
min="0"
max="8"
value="4"
with-markers
></ha-slider>
<span>Small</span>
<ha-slider
size="small"
min="0"
max="8"
value="4"
with-markers
></ha-slider>
<span>Medium</span>
<ha-slider
size="medium"
min="0"
max="8"
value="4"
with-markers
></ha-slider>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: 8px;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-slider": DemoHaSlider;
}
}

View File

@@ -6,23 +6,21 @@ A tooltip's target is its _first child element_, so you should only wrap one ele
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout. Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
<ha-button id="hover">Hover Me</ha-button> <ha-tooltip content="This is a tooltip">
<ha-tooltip for="hover"> <ha-button>Hover Me</ha-button>
This is a tooltip
</ha-tooltip> </ha-tooltip>
``` ```
<ha-button id="hover">Hover Me</ha-button> <ha-tooltip content="This is a tooltip">
<ha-tooltip for="hover"> <ha-button>Hover Me</ha-button>
This is a tooltip
</ha-tooltip> </ha-tooltip>
``` ```
## Documentation ## Documentation
This element is based on webawesome `wa-tooltip` it only sets some css tokens and has a custom show/hide animation. This element is based on shoelace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
<a href="https://webawesome.com/docs/components/tooltip/" target="_blank" rel="noopener noreferrer">Webawesome documentation</a> <a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
### HA style tokens ### HA style tokens
@@ -30,7 +28,7 @@ In your theme settings use this without the prefixed `--`.
- `--ha-tooltip-border-radius` (Default: 4px) - `--ha-tooltip-border-radius` (Default: 4px)
- `--ha-tooltip-arrow-size` (Default: 8px) - `--ha-tooltip-arrow-size` (Default: 8px)
- `--wa-tooltip-font-family` (Default: `var(--ha-font-family-body)`) - `--sl-tooltip-font-family` (Default: `var(--ha-font-family-body)`)
- `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`) - `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`)
- `--wa-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`) - `--sl-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`)
- `--wa-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`) - `--sl-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`)

View File

@@ -11,7 +11,6 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons"; import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate"; import { ClimateEntityFeature } from "../../../../src/data/climate";
import { FanEntityFeature } from "../../../../src/data/fan";
const ENTITIES = [ const ENTITIES = [
getEntity("switch", "tv_outlet", "on", { getEntity("switch", "tv_outlet", "on", {
@@ -101,15 +100,6 @@ const ENTITIES = [
ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
}), }),
getEntity("fan", "fan_demo", "on", {
friendly_name: "Ceiling fan",
device_class: "fan",
direction: "reverse",
supported_features:
FanEntityFeature.DIRECTION +
FanEntityFeature.SET_SPEED +
FanEntityFeature.OSCILLATE,
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -271,33 +261,6 @@ const CONFIGS = [
- type: target-temperature - type: target-temperature
`, `,
}, },
{
heading: "Fan direction feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-direction
`,
},
{
heading: "Fan speed feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-speed
`,
},
{
heading: "Fan oscillate feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-oscillate
`,
},
]; ];
@customElement("demo-lovelace-tile-card") @customElement("demo-lovelace-tile-card")

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import type { ActionHandlerEvent } from "../../../../src/data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../../src/data/lovelace/action_handler";
import { actionHandler } from "../../../../src/panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../../../../src/panels/lovelace/common/directives/action-handler-directive";
@@ -13,16 +13,12 @@ export class DemoUtilLongPress extends LitElement {
${[1, 2, 3].map( ${[1, 2, 3].map(
() => html` () => html`
<ha-card> <ha-card>
<ha-button <mwc-button
appearance="plain"
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({})}
hasHold: true,
hasDoubleClick: true,
})}
> >
(long) press me! (long) press me!
</ha-button> </mwc-button>
<textarea></textarea> <textarea></textarea>

View File

@@ -1,3 +0,0 @@
---
title: Fan
---

View File

@@ -1,50 +0,0 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { FanEntityFeature } from "../../../../src/data/fan";
const ENTITIES = [
getEntity("fan", "fan", "on", {
friendly_name: "Fan",
device_class: "fan",
supported_features:
FanEntityFeature.OSCILLATE +
FanEntityFeature.DIRECTION +
FanEntityFeature.SET_SPEED,
}),
];
@customElement("demo-more-info-fan")
class DemoMoreInfoFan extends LitElement {
@property({ attribute: false }) public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-fan": DemoMoreInfoFan;
}
}

View File

@@ -1,4 +0,0 @@
import { availableParallelism } from "node:os";
import "./build-scripts/gulp/index.mjs";
process.env.UV_THREADPOOL_SIZE = availableParallelism();

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-hassio yarn run-task build-hassio

View File

@@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-hassio yarn run-task develop-hassio

View File

@@ -149,7 +149,7 @@ class HassioAddonConfig extends LitElement {
) )
); );
private _filteredSchema = memoizeOne( private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => (options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required) schema.filter((entry) => entry.name in options || entry.required)
); );
@@ -161,7 +161,7 @@ class HassioAddonConfig extends LitElement {
showForm && showForm &&
JSON.stringify(this.addon.schema) !== JSON.stringify(this.addon.schema) !==
JSON.stringify( JSON.stringify(
this._filteredSchema(this.addon.options, this.addon.schema!) this._filteredShchema(this.addon.options, this.addon.schema!)
); );
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
@@ -199,7 +199,6 @@ class HassioAddonConfig extends LitElement {
<div class="card-content"> <div class="card-content">
${showForm ${showForm
? html`<ha-form ? html`<ha-form
.hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
.data=${this._options!} .data=${this._options!}
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
@@ -208,7 +207,7 @@ class HassioAddonConfig extends LitElement {
.schema=${this._convertSchema( .schema=${this._convertSchema(
this._showOptional this._showOptional
? this.addon.schema! ? this.addon.schema!
: this._filteredSchema( : this._filteredShchema(
this.addon.options, this.addon.options,
this.addon.schema! this.addon.schema!
) )

View File

@@ -99,8 +99,7 @@ class HassioAddonNetwork extends LitElement {
: nothing} : nothing}
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
variant="danger" class="warning"
appearance="plain"
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._resetTapped} @click=${this._resetTapped}
> >

View File

@@ -25,7 +25,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
@@ -188,13 +187,12 @@ class HassioAddonInfo extends LitElement {
"addon.dashboard.protection_mode.content" "addon.dashboard.protection_mode.content"
)} )}
<ha-button <ha-button
variant="danger"
slot="action" slot="action"
@click=${this._protectionToggled} .label=${this.supervisor.localize(
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable" "addon.dashboard.protection_mode.enable"
)} )}
@click=${this._protectionToggled}
>
</ha-button> </ha-button>
</ha-alert> </ha-alert>
` `
@@ -694,16 +692,14 @@ class HassioAddonInfo extends LitElement {
? this._computeIsRunning ? this._computeIsRunning
? html` ? html`
<ha-progress-button <ha-progress-button
variant="danger" class="warning"
appearance="plain"
@click=${this._stopClicked} @click=${this._stopClicked}
.disabled=${systemManaged && !this.controlEnabled} .disabled=${systemManaged && !this.controlEnabled}
> >
${this.supervisor.localize("addon.dashboard.stop")} ${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button> </ha-progress-button>
<ha-progress-button <ha-progress-button
variant="danger" class="warning"
appearance="plain"
@click=${this._restartClicked} @click=${this._restartClicked}
> >
${this.supervisor.localize("addon.dashboard.restart")} ${this.supervisor.localize("addon.dashboard.restart")}
@@ -713,60 +709,10 @@ class HassioAddonInfo extends LitElement {
<ha-progress-button <ha-progress-button
@click=${this._startClicked} @click=${this._startClicked}
.progress=${this.addon.state === "startup"} .progress=${this.addon.state === "startup"}
appearance="plain"
> >
${this.supervisor.localize("addon.dashboard.start")} ${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button> </ha-progress-button>
` `
: nothing}
</div>
<div>
${this.addon.version
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._rebuildClicked}
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}
${this._computeShowWebUI || this._computeShowIngressUI
? html`
<ha-button
href=${ifDefined(
!this._computeShowIngressUI
? this._pathWebui!
: nothing
)}
target=${ifDefined(
!this._computeShowIngressUI ? "_blank" : nothing
)}
rel=${ifDefined(
!this._computeShowIngressUI ? "noopener" : nothing
)}
@click=${!this._computeShowWebUI
? this._openIngress
: undefined}
>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
`
: html` : html`
<ha-progress-button <ha-progress-button
.disabled=${!this.addon.available} .disabled=${!this.addon.available}
@@ -776,12 +722,58 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button> </ha-progress-button>
`} `}
</div> </div>
<div>
${this.addon.version
? html` ${this._computeShowWebUI
? html`
<a
href=${this._pathWebui!}
tabindex="-1"
target="_blank"
rel="noopener"
>
<ha-button>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
</a>
`
: nothing}
${this._computeShowIngressUI
? html`
<ha-button @click=${this._openIngress}>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
<ha-progress-button
class="warning"
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
class="warning"
@click=${this._rebuildClicked}
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}`
: nothing}
</div>
</div> </div>
</ha-card> </ha-card>
${this.addon.long_description ${this.addon.long_description
? html` ? html`
<ha-card class="long-description" outlined> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<ha-markdown <ha-markdown
.content=${this.addon.long_description} .content=${this.addon.long_description}
@@ -1154,17 +1146,15 @@ class HassioAddonInfo extends LitElement {
), ),
dismissText: this.supervisor.localize("common.cancel"), dismissText: this.supervisor.localize("common.cancel"),
}); });
button.actionError();
button.progress = false; button.progress = false;
return; return;
} }
} catch (err: any) { } catch (err: any) {
button.actionError();
button.progress = false;
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to validate addon configuration", title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
button.progress = false;
return; return;
} }
@@ -1178,15 +1168,11 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) { } catch (err: any) {
button.actionError();
button.progress = false;
showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.start"), title: this.supervisor.localize("addon.dashboard.action_error.start"),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
return;
} }
button.actionSuccess();
button.progress = false; button.progress = false;
} }
@@ -1242,7 +1228,6 @@ class HassioAddonInfo extends LitElement {
path: "uninstall", path: "uninstall",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
button.actionSuccess();
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
@@ -1250,7 +1235,6 @@ class HassioAddonInfo extends LitElement {
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
button.actionError();
} }
button.progress = false; button.progress = false;
} }
@@ -1333,9 +1317,6 @@ class HassioAddonInfo extends LitElement {
.description a { .description a {
color: var(--primary-color); color: var(--primary-color);
} }
.long-description {
direction: ltr;
}
ha-assist-chip { ha-assist-chip {
--md-sys-color-primary: var(--text-primary-color); --md-sys-color-primary: var(--text-primary-color);
--md-sys-color-on-surface: var(--text-primary-color); --md-sys-color-on-surface: var(--text-primary-color);

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
@@ -16,7 +17,6 @@ import type {
} from "../../../src/components/data-table/ha-data-table"; } from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item"; import "../../../src/components/ha-list-item";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
@@ -241,13 +241,12 @@ export class HassioBackups extends LitElement {
<div class="header-btns"> <div class="header-btns">
${!this.narrow ${!this.narrow
? html` ? html`
<ha-button <mwc-button
appearance="plain"
variant="danger"
@click=${this._deleteSelected} @click=${this._deleteSelected}
class="warning"
> >
${this.supervisor.localize("backup.delete_selected")} ${this.supervisor.localize("backup.delete_selected")}
</ha-button> </mwc-button>
` `
: html` : html`
<ha-icon-button <ha-icon-button
@@ -409,7 +408,7 @@ export class HassioBackups extends LitElement {
margin-inline-end: -12px; margin-inline-end: -12px;
margin-inline-start: initial; margin-inline-start: initial;
} }
.header-btns > ha-button, .header-btns > mwc-button,
.header-btns > ha-icon-button { .header-btns > ha-icon-button {
margin: 8px; margin: 8px;
} }

View File

@@ -1,9 +1,10 @@
import "@material/mwc-button";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import type { HassioHassOSInfo } from "../../../src/data/hassio/host"; import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
@@ -108,9 +109,10 @@ export class HassioUpdate extends LitElement {
</ha-settings-row> </ha-settings-row>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-button appearance="plain" href="/hassio/update-available/${key}"> <a href="/hassio/update-available/${key}">
${this.supervisor.localize("common.show")} <mwc-button .label=${this.supervisor.localize("common.show")}>
</ha-button> </mwc-button>
</a>
</div> </div>
</ha-card> </ha-card>
`; `;

View File

@@ -1,10 +1,10 @@
import "@material/mwc-button/mwc-button";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types"; import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -77,21 +77,20 @@ class HassioBackupLocationDialog extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dialogInitialFocus dialogInitialFocus
></ha-form> ></ha-form>
<ha-button <mwc-button
appearance="plain"
slot="secondaryAction" slot="secondaryAction"
@click=${this.closeDialog} @click=${this.closeDialog}
dialogInitialFocus dialogInitialFocus
> >
${this._dialogParams.supervisor.localize("common.cancel")} ${this._dialogParams.supervisor.localize("common.cancel")}
</ha-button> </mwc-button>
<ha-button <mwc-button
.disabled=${this._waiting || !this._data} .disabled=${this._waiting || !this._data}
slot="primaryAction" slot="primaryAction"
@click=${this._changeMount} @click=${this._changeMount}
> >
${this._dialogParams.supervisor.localize("common.save")} ${this._dialogParams.supervisor.localize("common.save")}
</ha-button> </mwc-button>
</ha-dialog> </ha-dialog>
`; `;
} }

View File

@@ -8,6 +8,7 @@ import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import { slugify } from "../../../../src/common/string/slugify"; import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button"; import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";

View File

@@ -1,9 +1,10 @@
import "@material/mwc-button";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-spinner"; import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import { import {
@@ -68,20 +69,16 @@ class HassioCreateBackupDialog extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
<ha-button <mwc-button slot="secondaryAction" @click=${this.closeDialog}>
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this._dialogParams.supervisor.localize("common.close")} ${this._dialogParams.supervisor.localize("common.close")}
</ha-button> </mwc-button>
<ha-button <mwc-button
.disabled=${this._creatingBackup} .disabled=${this._creatingBackup}
slot="primaryAction" slot="primaryAction"
@click=${this._createBackup} @click=${this._createBackup}
> >
${this._dialogParams.supervisor.localize("backup.create")} ${this._dialogParams.supervisor.localize("backup.create")}
</ha-button> </mwc-button>
</ha-dialog> </ha-dialog>
`; `;
} }

Some files were not shown because too many files have changed in this diff Show More