diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a08fe58ea..ea9a32a30 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,28 +1,31 @@ --- -description: 'Guidelines for building C# and TypeScript applications' -applyTo: '**/*.cs, **/*.ts, **/*.js, **/*.css' +description: "Guidelines for building C# and TypeScript applications" +applyTo: "**/*.cs, **/*.ts, **/*.js, **/*.css" --- # Project Context + - This repository contains PWABuilder, a website for software developers that helps web devs create progressive web apps (PWAs) and publish them to app stores. -- The main repository is for PWABuilder.com, the web app. Its code is located in `apps/pwabuilder`. It is a C# web app using ASP.NET Core. It serves a SPA frontend, located in `apps/pwabuilder/Frontend`, which is built with TypeScript, Lit, and Shoelace. The frontend uses the Vite build system. It communicates with the backend via a REST API. +- The main repository is for PWABuilder.com, the web app. Its code is located in `apps/pwabuilder`. It is a C# web app using ASP.NET Core. It serves a SPA frontend, located in `apps/pwabuilder/Frontend`, which is built with TypeScript, Lit, and WebAwesome. The frontend uses the Vite build system. It communicates with the backend via a REST API. - The repository also contains some PWABuilder-related tooling. For example, `/apps/cli` contains the PWABuilder command line app, `/apps/pwabuilder-vscode` contains VSCode tooling for PWABuilder, `/docs` contains the documentation web site (docs.pwabuilder.com) for PWABuilder. - Frontend is built with Typescript for type safety - Frontend uses Lit web components for custom elements and SPA pages. -- Frontend uses the extrnal Shoelace library for prebuilt, styled components. +- Frontend uses the extrnal [WebAwesome library](https://webawesome.com/docs) for prebuilt, styled components. ## Typescript Development + - Enable strict mode in tsconfig.json for maximum type safety - Define types for method returns - Implement generic components and composables where applicable - New web components should inherit from LitElement -- Where possible, prefer to use existing web components from the Shoelace library +- Where possible, prefer to use existing web components from the [WebAwesome library](https://webawesome.com/docs/components) - Adhere to the single responsibility principle for components - For TypeScript files, use kebab-case for file names. - For custom web components built with Lit, keep them small and focused on one concern. - All TypeScript classes should be PascalCase. ## C# Development + - When writing C#, always use the latest version C#, currently C# 13 features. - Write clear and concise comments for each function. - Use file-scoped namespaces and single-line using directives. @@ -42,10 +45,11 @@ applyTo: '**/*.cs, **/*.ts, **/*.js, **/*.css' - Do not use LINQ query syntax (e.g. `from x in y select x`). Always prefer LINQ method syntax (`.Select()`, `.Where()`, ...) for consistency and clarity. - Do not insert more than one consecutive empty line, keep whitespace intentional and minimal. - Avoid excessive comments. Only add comments when the logic is non-obvious or complex — clean code should explain itself. -- All constructors, methods, events and properties must include XML documentation to support maintainability and developer tooling. +- All constructors, methods, events and properties must include XML documentation to support maintainability and developer tooling. - When the XML documentation is already defined in an interface or base class, use only `/// ` to inherit the documentation instead of duplicating it. ## General Instructions + - Make only high confidence suggestions when reviewing code changes. - Write code with good maintainability practices, including comments on why certain design decisions were made. - Handle edge cases and write clear exception handling. @@ -70,4 +74,4 @@ applyTo: '**/*.cs, **/*.ts, **/*.js, **/*.css' - Explain integration testing approaches for API endpoints. - Demonstrate how to mock dependencies for effective testing. - Show how to test authentication and authorization logic. -- Explain test-driven development principles as applied to API development. \ No newline at end of file +- Explain test-driven development principles as applied to API development. diff --git a/.github/workflows/deploy-pwabuilder-to-preview.yml b/.github/workflows/deploy-pwabuilder-to-preview.yml index f7a97fa7f..efafca160 100644 --- a/.github/workflows/deploy-pwabuilder-to-preview.yml +++ b/.github/workflows/deploy-pwabuilder-to-preview.yml @@ -1,8 +1,9 @@ name: Deploy PWABuilder web app to staging on: - push: + pull_request: branches: [main] + types: [opened, synchronize, reopened] paths: - "apps/pwabuilder/**" - "Dockerfile.production" diff --git a/.gitignore b/.gitignore index 38eea0573..1a55ba0df 100644 --- a/.gitignore +++ b/.gitignore @@ -388,3 +388,4 @@ test-results/ # Ingore service worker in wwwroot, as it's generated by the build. The real service worker is in /apps/pwabuilder/frontend/public/service-worker.js. apps/pwabuilder/wwwroot/service-worker.js +.spiderloop/ diff --git a/apps/blog/src/docs/windows/finding-publisher-info/finding-your-windows-publisher-info.md b/apps/blog/src/docs/windows/finding-publisher-info/finding-your-windows-publisher-info.md index da96e50e8..4529c3b9e 100644 --- a/apps/blog/src/docs/windows/finding-publisher-info/finding-your-windows-publisher-info.md +++ b/apps/blog/src/docs/windows/finding-publisher-info/finding-your-windows-publisher-info.md @@ -24,7 +24,7 @@ In order to publish your PWA in the Microsoft Store, you'll need 3 things: 2. `Publisher ID` 3. `Publisher display name` -To get this information, go to [Windows Partner Center](https://partner.microsoft.com/dashboard) and click on your app. (Don't have an app yet? [Create one](/docs/publish-a-new-app-to-the-microsoft-store/).) +To get this information, go to [Microsoft Partner Center](https://partner.microsoft.com/dashboard) and click on your app. (Don't have an app yet? [Create one](/docs/publish-a-new-app-to-the-microsoft-store/).) Choose `Product Management` -> `Product Identity`: diff --git a/apps/blog/src/docs/windows/image-recommendations/image-recommendations-for-windows.md b/apps/blog/src/docs/windows/image-recommendations/image-recommendations-for-windows.md index 520a147b4..5fe12b0ba 100644 --- a/apps/blog/src/docs/windows/image-recommendations/image-recommendations-for-windows.md +++ b/apps/blog/src/docs/windows/image-recommendations/image-recommendations-for-windows.md @@ -238,7 +238,7 @@ Shown in the start menu when the user sets your app's tile to large size. ### Store logo The store logo icon -Shown in app installer, Windows Partner Center, the "Report an app" option in the Store, and the "Write a review" option in the Store. +Shown in app installer, Microsoft Partner Center, the "Report an app" option in the Store, and the "Write a review" option in the Store. - 50x50 - 63x63 (1.25x scale) diff --git a/apps/blog/src/docs/windows/next-steps/next-steps.md b/apps/blog/src/docs/windows/next-steps/next-steps.md index 26978a133..a2d47757b 100644 --- a/apps/blog/src/docs/windows/next-steps/next-steps.md +++ b/apps/blog/src/docs/windows/next-steps/next-steps.md @@ -46,9 +46,10 @@ Your zip file [contains 2 app packages](/docs/what-is-a-classic-package/): - `{app name}.msixbundle` - the main app package - `{app name}.classic.appxbundle` - app package that allows users on older versions of Windows (below 10.0.19041, May 2020 Update) to run your app. See our [classic app package explainer](/docs/what-is-a-classic-package/) for details. -Both packages can be submitted directly to the Microsoft Store through the [Windows Partner Center](https://partner.microsoft.com/dashboard) +Both packages can be submitted directly to the Microsoft Store through the [Microsoft Partner Center](https://partner.microsoft.com/dashboard) When you're ready to publish to the Store, you can either + - [Publish a new app in the Store](/docs/publish-a-new-app-to-the-microsoft-store/) - [Update an existing app in the Store](/docs/update-an-existing-app-in-the-microsoft-store/) diff --git a/apps/blog/src/docs/windows/publish-new-windows-app/publish-new-windows-app.md b/apps/blog/src/docs/windows/publish-new-windows-app/publish-new-windows-app.md index 2642af6ea..7db466b07 100644 --- a/apps/blog/src/docs/windows/publish-new-windows-app/publish-new-windows-app.md +++ b/apps/blog/src/docs/windows/publish-new-windows-app/publish-new-windows-app.md @@ -22,7 +22,7 @@ This guide shows how to publish your PWA as a new app in the Store. If you alrea ## Enroll in Windows developer program -Login to [Windows Partner Center](https://partner.microsoft.com/dashboard) with your Microsoft account. +Login to [Microsoft Partner Center](https://partner.microsoft.com/dashboard) with your Microsoft account. Then, go to your [dashboard](https://partner.microsoft.com/en-us/dashboard/home) to see your enrolled programs. If `Windows & Xbox` is listed, great, you're already enrolled. If not, choose `Add program`:
A screenshot that shows where the add program button is diff --git a/apps/blog/src/docs/windows/update-existing-app/update-an-existing-app.md b/apps/blog/src/docs/windows/update-existing-app/update-an-existing-app.md index 260b23e0c..a8e36901c 100644 --- a/apps/blog/src/docs/windows/update-existing-app/update-an-existing-app.md +++ b/apps/blog/src/docs/windows/update-existing-app/update-an-existing-app.md @@ -22,7 +22,7 @@ This guide shows how to publish your PWA as an update to an existing app in the ## Choose the app you want to update -Login to [Windows Partner Center](https://partner.microsoft.com/dashboard), then go to `Windows & Xbox` -> `Overview`. Your existing apps will be listed: +Login to [Microsoft Partner Center](https://partner.microsoft.com/dashboard), then go to `Windows & Xbox` -> `Overview`. Your existing apps will be listed: A screenshot that shows your existing apps list diff --git a/apps/blog/src/docs/windows/windows-store-documentation/windows-store-documentation.md b/apps/blog/src/docs/windows/windows-store-documentation/windows-store-documentation.md index 41e735d6f..12beccac7 100644 --- a/apps/blog/src/docs/windows/windows-store-documentation/windows-store-documentation.md +++ b/apps/blog/src/docs/windows/windows-store-documentation/windows-store-documentation.md @@ -26,7 +26,7 @@ Both the Microsoft Store and Windows itself natively supports PWAs as first-clas Follow these steps to get your PWA listed in the Microsoft Store: -1. Reserve an app name in the [Windows Partner Center](https://partner.microsoft.com/dashboard) as explained in this documentation [Publish your Progressive Web App to the Microsoft Store](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/microsoft-store) +1. Reserve an app name in the [Microsoft Partner Center](https://partner.microsoft.com/dashboard) as explained in this documentation [Publish your Progressive Web App to the Microsoft Store](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/microsoft-store) 2. Create your Microsoft Store app package on [PWA Builder](https://www.pwabuilder.com) 3. Follow [the Next steps for getting your PWA into the Microsoft Store](/docs/next-steps-for-getting-your-pwa-into-the-microsoft-store/) diff --git a/apps/pwabuilder-chromium-extension/src/components/package-windows.ts b/apps/pwabuilder-chromium-extension/src/components/package-windows.ts index c6a0dcf12..06996af48 100644 --- a/apps/pwabuilder-chromium-extension/src/components/package-windows.ts +++ b/apps/pwabuilder-chromium-extension/src/components/package-windows.ts @@ -10,11 +10,11 @@ import '@shoelace-style/shoelace/dist/components/tooltip/tooltip'; @customElement("package-windows") export class PackageWindows extends LitElement { - @state() currentManiUrl: string | undefined = undefined; - @state() windowsOptions: WindowsOptions | undefined = undefined; - - static styles = [ - css` + @state() currentManiUrl: string | undefined = undefined; + @state() windowsOptions: WindowsOptions | undefined = undefined; + + static styles = [ + css` :host { display: block; } @@ -45,144 +45,144 @@ export class PackageWindows extends LitElement { justify-content: space-between; } `, - ]; - - private _siteData!: SiteData; - - @property() - get siteData(): SiteData { - return this._siteData; - } - - set siteData(val: SiteData) { - const oldValue = this._siteData; - this._siteData = val; - - if (this._siteData && this._siteData.currentUrl && this._siteData.manifest.hasManifest) { - this.currentManiUrl = this._siteData.manifest.manifestUri; - const manifest = this._siteData.manifest.manifest; - - if (manifest) { - // set options we can find in manifest - this.setUpOptions( - this._siteData.currentUrl, - manifest.name || manifest.short_name || "My App", - "", - "", - "", - true, - "", - "" - ); - } - } + ]; + + private _siteData!: SiteData; - this.requestUpdate(); - } - - - private async packageForWindows(options: any): Promise { - let response: Response | undefined; - - try { - response = await fetch(windowsEndpoint, { - method: "POST", - body: JSON.stringify(options), - headers: new Headers({ "content-type": "application/json" }), - }); - } catch (err) { - console.error(err); + @property() + get siteData(): SiteData { + return this._siteData; } - return response; - } - - private setUpOptions( - url: string, - name: string, - packageId: string, - version: string, - classicVersion: string, - allowSigning: boolean = true, - publisherDisplayName: string, - publisherCommonName: string - ): WindowsOptions { - this.windowsOptions = { - url, - name, - packageId, - version, - allowSigning, - classicPackage: { - generate: true, - version: classicVersion, - }, - publisher: { - displayName: publisherDisplayName, - commonName: publisherCommonName, - }, - }; - - return this.windowsOptions; - } - - async handleSubmit(event: SubmitEvent) { - event.preventDefault(); - - if (event.target) { - let data = new FormData(event.target as HTMLFormElement); - - if (this.windowsOptions) { - for (var pair of data.entries()) { - switch (pair[0]) { - case "packageId": - this.windowsOptions.packageId = pair[1].toString(); - - break; - case "version": - this.windowsOptions.version = pair[1].toString(); - - break; - case "classicVersion": - this.windowsOptions.classicPackage.version = pair[1].toString(); - - break; - case "publisherDisplayName": - this.windowsOptions.publisher.displayName = pair[1].toString(); - - break; - case "publisherCommonName": - this.windowsOptions.publisher.commonName = pair[1].toString(); - - break; - - default: - break; - } + set siteData(val: SiteData) { + const oldValue = this._siteData; + this._siteData = val; + + if (this._siteData && this._siteData.currentUrl && this._siteData.manifest.hasManifest) { + this.currentManiUrl = this._siteData.manifest.manifestUri; + const manifest = this._siteData.manifest.manifest; + + if (manifest) { + // set options we can find in manifest + this.setUpOptions( + this._siteData.currentUrl, + manifest.name || manifest.short_name || "My App", + "", + "", + "", + true, + "", + "" + ); + } } - // we now have all options, lets try to generate a package - const response = await this.packageForWindows(this.windowsOptions); - if (response) { - const data = await response.blob(); - const url = URL.createObjectURL(data); + this.requestUpdate(); + } + + + private async packageForWindows(options: any): Promise { + let response: Response | undefined; - // var blob = new Blob(["text" + "file"], {type: "application/octet-stream"}); - // var url = URL.createObjectURL(blob); + try { + response = await fetch(windowsEndpoint, { + method: "POST", + body: JSON.stringify(options), + headers: new Headers({ "content-type": "application/json" }), + }); + } catch (err) { + console.error(err); + } + + return response; + } - // download the package - await chrome.downloads.download({ + private setUpOptions( + url: string, + name: string, + packageId: string, + version: string, + classicVersion: string, + allowSigning: boolean = true, + publisherDisplayName: string, + publisherCommonName: string + ): WindowsOptions { + this.windowsOptions = { url, - filename: `${this.windowsOptions.packageId}.zip`, - saveAs: true, - }); + name, + packageId, + version, + allowSigning, + classicPackage: { + generate: true, + version: classicVersion, + }, + publisher: { + displayName: publisherDisplayName, + commonName: publisherCommonName, + }, + }; + + return this.windowsOptions; + } + + async handleSubmit(event: SubmitEvent) { + event.preventDefault(); + + if (event.target) { + let data = new FormData(event.target as HTMLFormElement); + + if (this.windowsOptions) { + for (var pair of data.entries()) { + switch (pair[0]) { + case "packageId": + this.windowsOptions.packageId = pair[1].toString(); + + break; + case "version": + this.windowsOptions.version = pair[1].toString(); + + break; + case "classicVersion": + this.windowsOptions.classicPackage.version = pair[1].toString(); + + break; + case "publisherDisplayName": + this.windowsOptions.publisher.displayName = pair[1].toString(); + + break; + case "publisherCommonName": + this.windowsOptions.publisher.commonName = pair[1].toString(); + + break; + + default: + break; + } + } + + // we now have all options, lets try to generate a package + const response = await this.packageForWindows(this.windowsOptions); + if (response) { + const data = await response.blob(); + const url = URL.createObjectURL(data); + + // var blob = new Blob(["text" + "file"], {type: "application/octet-stream"}); + // var url = URL.createObjectURL(blob); + + // download the package + await chrome.downloads.download({ + url, + filename: `${this.windowsOptions.packageId}.zip`, + saveAs: true, + }); + } + } } - } } - } - render() { - return html` + render() { + return html`

Package for The Microsoft Store

Documentation @@ -193,7 +193,7 @@ export class PackageWindows extends LitElement {
The Package ID uniquely identifying your app in the Microsoft Store. - Get this value from Windows Partner Center. + Get this value from Microsoft Partner Center.
- -
- -
- ${this.renderFormInput({ - label: 'Enable', - tooltip: 'If enabled, your download will include the source code for your Android app.', - inputId: 'include-src-input', - type: 'checkbox', - checked: this.packageOptions.includeSourceCode === true, - inputHandler: (_, checked) => this.packageOptions.includeSourceCode = checked - })} -
-
- this.rotateNinety()} @sl-hide=${() => this.rotateZero()}> + this.rotateNinety()} @wa-hide=${() => this.rotateZero()}>
-

All Settings

- dropdown toggler + All Settings
@@ -566,44 +472,46 @@ export class AndroidForm extends AppPackageFormBase {
- -
- ${this.renderFormInput({ - label: 'Standalone', - tooltip: 'Recommended for most apps. The Android status bar and navigation bar will be shown while your app is running.', - tooltipLink: 'https://developer.android.com/training/system-ui/immersive', - inputId: 'display-standalone-input', - type: 'radio', - name: 'displayMode', - value: 'standalone', - checked: this.packageOptions.display === 'standalone', - inputHandler: () => this.packageOptions.display = 'standalone' - })} -
+
${this.renderFormInput({ - label: 'Fullscreen', - tooltip: `The Android status bar and navigation bar will be hidden while your app is running. Suitable for immersive experiences such as games or media apps.`, - tooltipLink: 'https://developer.android.com/training/system-ui/immersive#immersive', - inputId: 'display-fullscreen-input', - type: 'radio', - name: 'displayMode', - value: 'fullscreen', - checked: this.packageOptions.display === 'fullscreen', - inputHandler: () => this.packageOptions.display = 'fullscreen' + label: 'Enable', + tooltip: 'If enabled, your download will include the source code for your Android app.', + inputId: 'include-src-input', + type: 'checkbox', + checked: this.packageOptions.includeSourceCode === true, + inputHandler: (_, checked) => this.packageOptions.includeSourceCode = checked })}
+
+ +
+
- ${this.renderFormInput({ - label: 'Fullscreen sticky', - tooltip: `The Android status bar and navigation bar will be hidden while your app is running, and if the user swipes from the edge of the Android device, the system bars will be semi-transparent, and the touch gesture will be passed to your app. Recommended for drawing apps, and games that require lots of swiping.`, - tooltipLink: 'https://developer.android.com/training/system-ui/immersive#sticky-immersive', - inputId: 'display-fullscreen-sticky-input', - type: 'radio', + ${this.renderFormRadioGroup({ name: 'displayMode', - value: 'fullscreen-sticky', - checked: this.packageOptions.display === 'fullscreen-sticky', - inputHandler: () => this.packageOptions.display = 'fullscreen-sticky' + value: this.packageOptions.display ?? 'standalone', + valueChangedHandler: (value) => this.packageOptions.display = value as AndroidPackageOptions['display'], + radios: [ + { + label: 'Standalone', + value: 'standalone', + tooltip: 'Recommended for most apps. The Android status bar and navigation bar will be shown while your app is running.', + tooltipLink: 'https://developer.android.com/training/system-ui/immersive' + }, + { + label: 'Fullscreen', + value: 'fullscreen', + tooltip: `The Android status bar and navigation bar will be hidden while your app is running. Suitable for immersive experiences such as games or media apps.`, + tooltipLink: 'https://developer.android.com/training/system-ui/immersive#immersive' + }, + { + label: 'Fullscreen sticky', + value: 'fullscreen-sticky', + tooltip: `The Android status bar and navigation bar will be hidden while your app is running, and if the user swipes from the edge of the Android device, the system bars will be semi-transparent, and the touch gesture will be passed to your app. Recommended for drawing apps, and games that require lots of swiping.`, + tooltipLink: 'https://developer.android.com/training/system-ui/immersive#sticky-immersive' + } + ] })}
@@ -669,40 +577,28 @@ export class AndroidForm extends AppPackageFormBase {
- ${this.renderFormInput({ + ${this.renderFormRadioGroup({ + name: 'signingMode', + value: this.packageOptions.signingMode ?? 'new', + valueChangedHandler: (value) => this.androidSigningModeChanged(value as 'mine' | 'new' | 'none'), + radios: [ + { label: 'New', - tooltip: `Recommended for new apps in Google Play. PWABuilder will generate a new signing key for you and sign your package with it. Your download will contain the new signing details.`, - inputId: 'signing-new-input', - name: 'signingMode', value: 'new', - type: 'radio', - checked: this.packageOptions.signingMode === 'new', - inputHandler: () => this.androidSigningModeChanged('new') - })} -
-
- ${this.renderFormInput({ + tooltip: `Recommended for new apps in Google Play. PWABuilder will generate a new signing key for you and sign your package with it. Your download will contain the new signing details.` + }, + { label: 'Use mine', - tooltip: 'Recommended for existing apps in Google Play. Use this option if you already have a signing key and you want to publish a new version of an existing app in Google Play.', - inputId: 'signing-mine-input', - name: 'signingMode', value: 'mine', - type: 'radio', - checked: this.packageOptions.signingMode === 'mine', - inputHandler: () => this.androidSigningModeChanged('mine') - })} -
-
- ${this.renderFormInput({ + tooltip: 'Recommended for existing apps in Google Play. Use this option if you already have a signing key and you want to publish a new version of an existing app in Google Play.' + }, + { label: 'None', - tooltip: 'PWABuilder will generate a raw, unsigned APK. Raw, unsigned APKs cannot be uploaded to the Google Play Store.', - inputId: 'signing-none-input', - name: 'signingMode', value: 'none', - type: 'radio', - checked: this.packageOptions.signingMode === 'none', - inputHandler: () => this.androidSigningModeChanged('none') - })} + tooltip: 'PWABuilder will generate a raw, unsigned APK. Raw, unsigned APKs cannot be uploaded to the Google Play Store.' + } + ] + })}
@@ -745,34 +641,29 @@ export class AndroidForm extends AppPackageFormBase {
- ${this.renderFormInput({ + ${this.renderFormRadioGroup({ + name: 'fallbackType', + value: this.packageOptions.fallbackType ?? 'customtabs', + valueChangedHandler: (value) => this.packageOptions.fallbackType = value as AndroidPackageOptions['fallbackType'], + radios: [ + { label: 'Custom Tabs', - tooltip: `When Trusted Web Activity (TWA) is unavailable, use Chrome Custom Tabs as a fallback to run your app.`, - tooltipLink: 'https://developer.chrome.com/docs/android/custom-tabs/', - inputId: 'chrome-custom-tab-fallback-input', - type: 'radio', - name: 'fallbackType', value: 'customtabs', - checked: this.packageOptions.fallbackType === 'customtabs', - inputHandler: () => this.packageOptions.fallbackType = 'customtabs' - })} -
-
- ${this.renderFormInput({ + tooltip: `When Trusted Web Activity (TWA) is unavailable, use Chrome Custom Tabs as a fallback to run your app.`, + tooltipLink: 'https://developer.chrome.com/docs/android/custom-tabs/' + }, + { label: 'Web View', - tooltip: `When Trusted Web Activity (TWA) is unavailable, use a web view as a fallback to run your app.`, - tooltipLink: 'https://developer.chrome.com/docs/android/custom-tabs/', - inputId: 'web-view-fallback-input', - type: 'radio', - name: 'fallbackType', value: 'webview', - checked: this.packageOptions.fallbackType === 'webview', - inputHandler: () => this.packageOptions.fallbackType = 'webview' - })} + tooltip: `When Trusted Web Activity (TWA) is unavailable, use a web view as a fallback to run your app.`, + tooltipLink: 'https://developer.chrome.com/docs/android/custom-tabs/' + } + ] + })}
-
+ diff --git a/apps/pwabuilder/Frontend/src/script/components/app-button.styles.ts b/apps/pwabuilder/Frontend/src/script/components/app-button.styles.ts new file mode 100644 index 000000000..51c12289e --- /dev/null +++ b/apps/pwabuilder/Frontend/src/script/components/app-button.styles.ts @@ -0,0 +1,70 @@ +import { css } from "lit"; +import { mediumBreakPoint } from '../utils/css/breakpoints'; +import { fastButtonCss } from '../utils/css/fast-elements'; + +export const appButtonStyles = css` + :host { + border-radius: var(--button-radius); + display: block; + outline: none; + --font-size: var(--desktop-button-font-size); + --button-height: 44px; + --button-height: var(--desktop-button-height); + --button-square: var(--button-height); + --button-width: 127px; + --button-width: var(--button-width); + --button-font-color: var(--secondary-color); + --pading-vertical: 0; + --padding-horizontal: 34px; + } + + ${fastButtonCss} + + [appearance='lightweight'] { + box-shadow: none; + } + + fast-button.link { + --accent-foreground-active: var(--font-color); + --accent-foreground-hover: var(--font-color); + width: auto; + border-radius: unset; + box-shadow: none; + background-color: transparent; + } + + fast-button.link::part(control) { + --padding-horizontal: 0; + width: auto; + } + + fast-button.round, + fast-button.square { + height: var(--button-square); + width: var(--button-square); + } + + fast-button.round::part(control), + fast-button.square::part(control) { + /* assumption is that the button is 14x21 */ + --padding-horizontal: 15px; + align-items: center; + line-height: 0; + } + + fast-button:focus { + outline: solid; + outline-width: 2px; + } + + ${mediumBreakPoint( + css` + fast-button.navigation { + --button-width: 100px; + --button-height: 40px; + line-height: 28px; + font-size: 16px; + } + ` + )} +`; diff --git a/apps/pwabuilder/Frontend/src/script/components/app-button.ts b/apps/pwabuilder/Frontend/src/script/components/app-button.ts index 9103bd067..9c7d0f18f 100644 --- a/apps/pwabuilder/Frontend/src/script/components/app-button.ts +++ b/apps/pwabuilder/Frontend/src/script/components/app-button.ts @@ -1,10 +1,9 @@ -import { LitElement, css, html } from 'lit'; +import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { mediumBreakPoint } from '../utils/css/breakpoints'; -import { fastButtonCss } from '../utils/css/fast-elements'; import { FastButtonAppearance } from '../utils/fast-element'; import { AppButtonElement } from '../utils/interfaces.components'; +import { appButtonStyles } from './app-button.styles'; @customElement('app-button') export class AppButton extends LitElement implements AppButtonElement { @@ -13,71 +12,7 @@ export class AppButton extends LitElement implements AppButtonElement { @property({ type: String }) appearance: FastButtonAppearance = 'neutral'; @property({ type: Boolean }) disabled = false; - static get styles() { - return [ - css` - :host { - border-radius: var(--button-radius); - display: block; - outline: none; - --font-size: var(--desktop-button-font-size); - --button-height: 44px; - --button-height: var(--desktop-button-height); - --button-square: var(--button-height); - --button-width: 127px; - --button-width: var(--button-width); - --button-font-color: var(--secondary-color); - --pading-vertical: 0; - --padding-horizontal: 34px; - } - `, - // fast css - fastButtonCss, - css` - [appearance='lightweight'] { - box-shadow: none; - } - fast-button.link { - --accent-foreground-active: var(--font-color); - --accent-foreground-hover: var(--font-color); - width: auto; - border-radius: unset; - box-shadow: none; - background-color: transparent; - } - fast-button.link::part(control) { - --padding-horizontal: 0; - width: auto; - } - fast-button.round, - fast-button.square { - height: var(--button-square); - width: var(--button-square); - } - fast-button.round::part(control), - fast-button.square::part(control) { - /* assumption is that the button is 14x21 */ - --padding-horizontal: 15px; - align-items: center; - line-height: 0; - } - fast-button:focus { - outline: solid; - outline-width: 2px; - } - `, - mediumBreakPoint( - css` - fast-button.navigation { - --button-width: 100px; - --button-height: 40px; - line-height: 28px; - font-size: 16px; - } - ` - ), - ]; - } + static styles = [appButtonStyles]; constructor() { super(); diff --git a/apps/pwabuilder/Frontend/src/script/components/app-file-input.styles.ts b/apps/pwabuilder/Frontend/src/script/components/app-file-input.styles.ts new file mode 100644 index 000000000..ee7f72109 --- /dev/null +++ b/apps/pwabuilder/Frontend/src/script/components/app-file-input.styles.ts @@ -0,0 +1,17 @@ +import { css } from "lit"; +import { hidden } from '../utils/css/hidden'; +import { fastButtonCss } from '../utils/css/fast-elements'; + +export const appFileInputStyles = css` + [appearance='lightweight'] { + box-shadow: none; + } + + :hover { + background-color: transparent; + } + + ${hidden} + + ${fastButtonCss} +`; diff --git a/apps/pwabuilder/Frontend/src/script/components/app-file-input.ts b/apps/pwabuilder/Frontend/src/script/components/app-file-input.ts index 0a0fbe4d0..064a6c60e 100644 --- a/apps/pwabuilder/Frontend/src/script/components/app-file-input.ts +++ b/apps/pwabuilder/Frontend/src/script/components/app-file-input.ts @@ -1,13 +1,12 @@ -import { LitElement, css, html } from 'lit'; +import { LitElement, html } from 'lit'; import { customElement, property, state, query } from 'lit/decorators.js'; -import { hidden } from '../utils/css/hidden'; -import { fastButtonCss } from '../utils/css/fast-elements'; import { FileInputDetails, Lazy } from '../utils/interfaces'; import { ifDefined } from 'lit/directives/if-defined.js'; import { FileInputElement } from '../utils/interfaces.components'; -import '@shoelace-style/shoelace/dist/components/button/button.js'; +import { appFileInputStyles } from './app-file-input.styles'; +import '@awesome.me/webawesome/dist/components/button/button.js'; @customElement('app-file-input') export class FileInput extends LitElement implements FileInputElement { @@ -17,20 +16,7 @@ export class FileInput extends LitElement implements FileInputElement { @state() buttonText = 'Choose File'; - static get styles() { - return [ - css` - [appearance='lightweight'] { - box-shadow: none; - } - :hover { - background-color: transparent; - } - `, - hidden, - fastButtonCss, - ]; - } + static styles = [appFileInputStyles]; get input(): any { return this.fileInput; @@ -51,12 +37,12 @@ export class FileInput extends LitElement implements FileInputElement { render() { return html`
- ${this.buttonText} - + recordPWABuilderProcessStep(`footer.github_clicked`, AnalyticsBehavior.ProcessCheckpoint)} > - + recordPWABuilderProcessStep(`footer.twitter_clicked`, AnalyticsBehavior.ProcessCheckpoint)} > - + recordPWABuilderProcessStep(`footer.youtube_clicked`, AnalyticsBehavior.ProcessCheckpoint)} > - +
diff --git a/apps/pwabuilder/Frontend/src/script/components/app-header.styles.ts b/apps/pwabuilder/Frontend/src/script/components/app-header.styles.ts new file mode 100644 index 000000000..d95d4d42a --- /dev/null +++ b/apps/pwabuilder/Frontend/src/script/components/app-header.styles.ts @@ -0,0 +1,211 @@ +import { css } from "lit"; +import { + xxxLargeBreakPoint, + xxLargeBreakPoint, + xLargeBreakPoint, + largeBreakPoint, + mediumBreakPoint, + smallBreakPoint, + xSmallBreakPoint, +} from '../utils/css/breakpoints'; + +export const appHeaderStyles = css` + :host { + --header-background: white; + --header-border: rgba(0, 0, 0, 0.25) solid 1px; + } + + header { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 16px; + padding-right: 16px; + background: var(--header-background); + color: white; + height: 71px; + border-bottom: var(--header-border); + z-index: 1; + } + + header img { + cursor: pointer; + width: 100px; + height: auto; + } + + nav { + display: flex; + justify-content: flex-end; + align-items: center; + width: 8em; + gap: .75em; + } + + .nav_button { + all: unset; + } + + .nav_link { + color: var(--font-color); + text-decoration: none; + border-bottom: none; + font-weight: var(--font-bold); + font-size: var(--subheader-font-size); + margin: 0; + } + + .nav_link:focus { + outline: solid; + outline-width: 2px; + } + + .nav_link span { + display: inline-block; + height: 18px; + font-size: 20px; + } + + .nav_link:hover span{ + cursor: pointer; + } + + nav wa-icon { + font-size: 1.15em; + } + + .nav_link:visited { + color: black; + } + + .link { + display: block; + width: 100%; + text-decoration: none; + } + + .link:visited, .link:active, .link:link { + color: #777777; + } + + .hover-color:hover { + color: var(--primary-color); + } + + wa-dropdown::part(menu) { + display: flex; + flex-direction: column; + background-color: white; + font-size: 16px; + } + + wa-dropdown-item::part(checkmark), wa-dropdown-item::part(submenu-icon) { + display: none; + } + + wa-dropdown-item::part(label) { + text-decoration: none; + border-bottom: none; + font-size: 14px; + margin: 0; + padding: 0; + } + + wa-dropdown-item:focus-visible, wa-dropdown-item:hover { + background-color: transparent; + outline: none; + } + + wa-dropdown-item:focus-visible::part(label), wa-dropdown-item:hover::part(label) { + color: var(--primary-color); + background-color: transparent; + } + + wa-dropdown-item:focus-visible .link, wa-dropdown-item:hover .link { + color: var(--primary-color); + background-color: transparent; + } + + wa-dropdown { + position: relative; + } + + wa-dropdown::part(base){ + box-shadow: 0px 16px 24px 0px #00000026; + } + + .col-header { + text-decoration: none; + margin: 0; + white-space: nowrap; + font-weight: bold; + color: #777777; + padding: 0; + font-size: 14px; + } + + @media (prefers-color-scheme: light) { + header { + color: black; + } + } + + ${xSmallBreakPoint(css` + header { + padding-left: 8px; + padding-right: 8px; + } + + header img { + width: 60px; + } + + nav { + width: auto; + gap: 0.5em; + } + + .nav_link span { + font-size: 16px; + } + `)} + + ${smallBreakPoint(css` + `)} + + ${mediumBreakPoint(css` + header nav { + display: initial; + } + + #desktop-nav { + display: flex; + } + `)} + + ${largeBreakPoint(css` + #desktop-nav { + display: flex; + } + `)} + + ${xLargeBreakPoint(css` + header { + padding-left: 1em; + padding-right: 1em; + } + `)} + + ${xxLargeBreakPoint(css` + header { + padding-left: 3em; + padding-right: 3em; + } + `)} + + ${xxxLargeBreakPoint(css` + header { + background-color: white; + } + `)} +`; diff --git a/apps/pwabuilder/Frontend/src/script/components/app-header.ts b/apps/pwabuilder/Frontend/src/script/components/app-header.ts index 803f9d44b..8e0b1d246 100644 --- a/apps/pwabuilder/Frontend/src/script/components/app-header.ts +++ b/apps/pwabuilder/Frontend/src/script/components/app-header.ts @@ -1,290 +1,60 @@ import { Router } from '@vaadin/router'; -import { LitElement, css, html } from 'lit'; +import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { AnalyticsBehavior, recordPWABuilderProcessStep } from '../utils/analytics'; -import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js'; -import '@shoelace-style/shoelace/dist/components/menu/menu.js'; -import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js'; -import { - xxxLargeBreakPoint, - xxLargeBreakPoint, - xLargeBreakPoint, - largeBreakPoint, - mediumBreakPoint, - smallBreakPoint, - xSmallBreakPoint, -} from '../utils/css/breakpoints'; +import { appHeaderStyles } from './app-header.styles'; +import '@awesome.me/webawesome/dist/components/dropdown/dropdown.js'; +import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; +import "@awesome.me/webawesome/dist/components/icon/icon.js"; @customElement('app-header') export class AppHeader extends LitElement { - @property({ type: String }) page = 'home'; + @property({ type: String }) page = 'home'; - static get styles() { - return css` - :host { - --header-background: white; - --header-border: rgba(0, 0, 0, 0.25) solid 1px; - } + static styles = [appHeaderStyles]; - header { - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 16px; - padding-right: 16px; - background: var(--header-background); - color: white; - height: 71px; - - border-bottom: var(--header-border); - z-index: 1; - } - - header img { - cursor: pointer; - width: 100px; - height: auto; - } - - nav { - display: flex; - justify-content: flex-end; - align-items: center; - width: 8em; - gap: .75em; - } - - .nav_button { - all: unset; - } - - .nav_link { - color: var(--font-color); - text-decoration: none; - border-bottom: none; - font-weight: var(--font-bold); - font-size: var(--subheader-font-size); - margin: 0; - } - - .nav_link:focus { - outline: solid; - outline-width: 2px; - } - - .nav_link span { - display: inline-block; - height: 18px; - font-size: 20px; - } - - .nav_link:hover span{ - cursor: pointer; - } - - nav sl-icon { - font-size: 2em; - } - - .nav_link:visited { - color: black; - } - - .link { - display: block; - width: 100%; - text-decoration: none; - } - - .link:visited, .link:active, .link:link { - color: #777777; - } - - .hover-color:hover { - color: var(--primary-color); - } - - sl-menu { - display: flex; - flex-direction: column; - background-color: white; - gap: 5px; - color: #777777; - font-size: 16px; - border-radius: 5px; - height: fit-content; - width: 136px; - padding: 16px 22px; - } - - sl-menu-item::part(checked-icon), sl-menu-item::part(submenu-icon) { - display: none; - } - - sl-menu-item::part(base){ - color: #777777; - text-decoration: none; - border-bottom: none; - font-size: 14px; - margin: 0; - padding: 0; - } - - sl-menu-item::part(base):hover sl-menu-item::part(label) { - background-color: unset; - color: var(--primary-color); - } - - sl-menu-item:focus-visible::part(base) { - color: var(--primary-color); - background-color: transparent; - - } - - sl-menu-item:hover::part(base) { - color: var(--primary-color); - background-color: transparent; - font-weight: 700; - } - - sl-menu-item:focus-visible .link, sl-menu-item:hover .link { - color: var(--primary-color); - background-color: transparent; - font-weight: 700; - } - - sl-dropdown { - position: relative; - - } - sl-dropdown::part(base){ - box-shadow: 0px 16px 24px 0px #00000026; - } - - .col-header { - text-decoration: none; - margin: 0; - white-space: nowrap; - font-weight: bold; - color: #777777; - padding: 0; - font-size: 14px; - } - - @media (prefers-color-scheme: light) { - header { - color: black; - } - } - - ${xSmallBreakPoint(css` - header { - padding-left: 8px; - padding-right: 8px; - } - - header img { - width: 60px; - } - - nav { - width: auto; - gap: 0.5em; - } - - .nav_link span { - font-size: 16px; - } - `)} - - ${smallBreakPoint(css` - - `)} - - ${mediumBreakPoint(css` - header nav { - display: initial; - } - - #desktop-nav { - display: flex; - } - - `)} - - - ${largeBreakPoint(css` - #desktop-nav { - display: flex; - } - - `)} + constructor() { + super(); + } - ${xLargeBreakPoint(css` - header { - padding-left: 1em; - padding-right: 1em; - } - `)} + firstUpdated() { + // Cant seem to type `event` as a KeyboardEvent without TypeScript complaining + // with an error I dont fully understand. + // revisit: Justin + this.shadowRoot?.querySelector('#header-icon')?.addEventListener("keydown", (event) => { + // casting here because of type problem described above + if ((event as KeyboardEvent).key === "Enter") { + Router.go("/"); + } + }) + } - ${xxLargeBreakPoint(css` - header { - padding-left: 3em; - padding-right: 3em; - } - `)} + recordGoingHome() { + window.location.href = "/"; + recordPWABuilderProcessStep(`header.logo_clicked`, AnalyticsBehavior.ProcessCheckpoint); + } - ${xxxLargeBreakPoint(css` - header { - background-color: white; + showMenu() { + let menu = this.shadowRoot!.querySelector("wa-dropdown"); + if (menu!.open) { + recordPWABuilderProcessStep(`header.community_dropdown_closed`, AnalyticsBehavior.ProcessCheckpoint) + menu!.open = false; + } else { + recordPWABuilderProcessStep(`header.community_dropdown_expanded`, AnalyticsBehavior.ProcessCheckpoint) + menu!.open = true; } - `)} - `; - } - - constructor() { - super(); - } - - firstUpdated() { - // Cant seem to type `event` as a KeyboardEvent without TypeScript complaining - // with an error I dont fully understand. - // revisit: Justin - this.shadowRoot?.querySelector('#header-icon')?.addEventListener("keydown", (event) => { - // casting here because of type problem described above - if ((event as KeyboardEvent).key === "Enter") { - Router.go("/"); - } - }) - } - - recordGoingHome() { - window.location.href = "/"; - recordPWABuilderProcessStep(`header.logo_clicked`, AnalyticsBehavior.ProcessCheckpoint); - } - - showMenu(){ - let menu = this.shadowRoot!.querySelector("sl-dropdown"); - if(menu!.open){ - recordPWABuilderProcessStep(`header.community_dropdown_closed`, AnalyticsBehavior.ProcessCheckpoint) - menu!.hide() - } else { - recordPWABuilderProcessStep(`header.community_dropdown_expanded`, AnalyticsBehavior.ProcessCheckpoint) - menu!.show(); - } - } - // hacky work around for clicking links with keyboard that are nested in menu items - // in the future, shoelace may make a thing but for now this works. - handleClickingLink(linkTag: string, analyticsString: string){ - const anchor: HTMLAnchorElement = this.shadowRoot!.querySelector('[data-tag="' + linkTag + '"]')!; - anchor.click(); - recordPWABuilderProcessStep(analyticsString, AnalyticsBehavior.ProcessCheckpoint) - } + // hacky work around for clicking links with keyboard that are nested in menu items + // in the future, Web Awesome may make a thing but for now this works. + handleClickingLink(linkTag: string, analyticsString: string) { + const anchor: HTMLAnchorElement = this.shadowRoot!.querySelector('[data-tag="' + linkTag + '"]')!; + anchor.click(); + recordPWABuilderProcessStep(analyticsString, AnalyticsBehavior.ProcessCheckpoint) + } - render() { - return html` + render() { + return html`
- ${this.page === "home" ? - html` + ${this.page === "home" ? + html` Docs ` : null - } + } - + - - -

Follow us on

- this.handleClickingLink("github_link", "header.github_clicked")}> + this.handleClickingLink("github_link", "header.github_clicked")}> - Github + + PWABuilder on GitHub - - this.handleClickingLink("twitter_link", "header.twitter_clicked")}> + + this.handleClickingLink("twitter_link", "header.twitter_clicked")}> - X + + PWABuilder on X - - this.handleClickingLink("discord_link", "header.discord_clicked")}> + + this.handleClickingLink("discord_link", "header.discord_clicked")}> - Discord + + PWABuilder on Discord - -
-
+ +
`; - } + } } diff --git a/apps/pwabuilder/Frontend/src/script/components/app-package-form-base.styles.ts b/apps/pwabuilder/Frontend/src/script/components/app-package-form-base.styles.ts new file mode 100644 index 000000000..051b5cf88 --- /dev/null +++ b/apps/pwabuilder/Frontend/src/script/components/app-package-form-base.styles.ts @@ -0,0 +1,193 @@ +import { css } from "lit"; + +export const appPackageFormBaseStyles = css` + #form-layout input { + border: 1px solid rgba(194, 201, 209, 1); + border-radius: var(--input-border-radius); + color: var(--font-color); + } + + #form-layout input:not([type='color']) { + padding: 10px; + } + + input::placeholder { + color: var(--placeholder-color); + font-style: italic; + } + + #form-extras wa-button::part(base) { + background-color: var(--font-color); + color: #ffffff; + font-size: 14px; + height: 3em; + width: 25%; + border-radius: var(--button-border-radius); + } + + #form-extras wa-button::part(label){ + display: flex; + align-items: center; + } + + #form-layout { + overflow-y: auto; + padding: 0em 1.5em 0 1em; + } + + .tooltip { + margin-left: 10px; + } + + .form-group .tooltip a { + color: #fff; + } + + .form-group { + display: flex; + flex-direction: column; + } + + .form-group label { + font-size: var(--small-medium-font-size); + font-weight: bold; + line-height: 40px; + display: flex; + align-items: center; + } + + .form-group label a { + text-decoration: none; + color: var(--font-color); + } + + /* Inline required asterisk, matching WebAwesome's required-indicator tokens. */ + .required-indicator { + color: var(--wa-form-control-required-content-color); + margin-inline-start: var(--wa-form-control-required-content-offset); + } + + /* Suppress wa-input's built-in required asterisk: we render no label inside + wa-input, so its asterisk would otherwise appear alone on its own line. + We show the asterisk inline next to our own