diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c8c2d2aaf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.json] +indent_size = 2 + +[*.{html,js,md}] +block_comment_start = /** +block_comment = * +block_comment_end = */ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c01931261 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +## Project Overview + +**titanium-elements** is a monorepo that publishes [`@leavittsoftware/web`](packages/web) — Lit 3 + Material Web custom elements used across Leavitt Group front-end applications. + +- **`titanium-*`** — general-purpose UI components (drawers, data tables, inputs, snackbars, etc.) +- **`leavitt-*`** — domain-specific components (company/person selects, file explorer, app shell, API helpers); many require authentication and an `ApiService` instance + +The package is published to npm and consumed via **deep imports** (no barrel `index.ts` exports). + +## Tech Stack + +- **Framework:** Lit 3 — web components with Shadow DOM +- **UI base:** Material Web (`@material/web/*`) +- **Language:** TypeScript (strict, decorators enabled) +- **Shared types:** `@leavittsoftware/lg-core-typescript` for OData entity interfaces +- **Monorepo:** npm workspaces + Lerna (independent versioning) +- **Component docs:** Custom Elements Manifest → `packages/leavittbook/custom-elements.json` +- **Peer dependency:** `lit >= 3.0.0` + +## Monorepo Structure + +``` +packages/ + web/ # @leavittsoftware/web — all published elements (titanium/ + leavitt/) + leavittbook/ # Local component gallery and demos (not published); see packages/leavittbook/CLAUDE.md +``` + +Within `packages/web`: + +``` +titanium/ # General-purpose components, helpers, styles, events +leavitt/ # Domain components, app shell, api-service, auth +``` + +## Key Scripts + +```bash +npm start # tsc watch + web-dev-server + CEM watch +npm run build # tsc + tests + CEM + leavittbook rollup +npm run lint # prettier + eslint + lit-analyzer + tsc +npm run lint-fix # auto-fix formatting and lint issues +npm run cem # regenerate custom-elements-manifest +npm run lerna:publish # build + conventional publish to npm +``` + +Run `npm run lint` after edits and fix issues in changed files before committing. + +## Development Workflow + +1. `npm i` then `npm start` — browse component demos in leavittbook +2. **New component:** copy an existing folder under `packages/web`, add a leavittbook demo + route, add a tsconfig path +3. **Conventional Commits** required (`feat:`, `fix:`, `chore:`, etc.) +4. CEM output at `packages/leavittbook/custom-elements.json` helps verify APIs but is **not** shipped with `@leavittsoftware/web` + +## Import Convention + +Consumers register elements with side-effect imports: + +```ts +import '@leavittsoftware/web/titanium/drawer/drawer.js'; +import { TitaniumDrawer } from '@leavittsoftware/web/titanium/drawer/drawer.js'; +import { h2, p } from '@leavittsoftware/web/titanium/styles/styles.js'; +``` + +Paths mirror the source tree under `packages/web/`. Use `.js` extensions when importing from the published package. + +## Component Reference + +For per-component API (properties, methods, events, slots, CSS parts, and implementation notes), see [`packages/web/CLAUDE.md`](packages/web/CLAUDE.md). + +Most titanium/leavitt elements **extend or compose** `@material/web` components (`MdFilledTextField`, `md-dialog`, etc.). Mixins, validation, and `--md-*` styling often apply to those Material Web bases — see **Material Web foundation** in `packages/web/CLAUDE.md` before assuming a component is a plain `LitElement` wrapper. + +After install, consuming projects find the same file at: + +``` +node_modules/@leavittsoftware/web/CLAUDE.md +``` + +## Keeping `packages/web/CLAUDE.md` Up to Date + +This is a **required** part of every component change. The web-level `CLAUDE.md` is the primary reference for agents in consuming projects; stale docs are worse than no docs. + +Update [`packages/web/CLAUDE.md`](packages/web/CLAUDE.md) in the **same PR** whenever you: + +- Add, rename, or remove a component or public utility +- Change a component's public API (properties, attributes, methods, events, slots, CSS parts) +- Change cross-cutting behavior documented there (events, controllers, services, loading/snackbar patterns) +- Add or revise implementation gotchas that affect consumers +- Ship or change a **breaking** (`!`) release — add or update the **Upgrade changelog (for agents)** section + +**Checklist per change:** + +- [ ] Component entry added/updated/removed (primary catalog or internal appendix as appropriate) +- [ ] Cross-cutting section updated if the change affects shared patterns +- [ ] Import path and tag name match the source file +- [ ] **Upgrade changelog** entry added/updated for any `!` commit or semver-major publish (rename `### Unreleased` → `### X.Y.Z` on publish; add a fresh `### Unreleased` stub) + +`npm run cem` can help verify property lists against source, but purpose, usage notes, and gotchas must be curated manually. + +## Contributing Conventions + +- Extend `LitElement` or the appropriate `@material/web` base (e.g. `MdFilledTextField`); register with `@customElement('kebab-case-name')` (must include a hyphen) +- Use `@property() accessor` / `@state() accessor` (omit `accessor` on `@provide` / `@consume` context properties) +- Side-effect imports for element registration; deep paths mirror source tree +- Use `promiseTracking` for loading state on page components (`LoadWhile` mixin removed; `titanium-data-table-core.loadWhile()` remains as a deprecated alias for `trackLoadingPromise`) +- Pair `ShowSnackbarEvent` with `titanium-snackbar-stack`; `PendingStateEvent` with loading indicators +- Multi-word reflected attributes use **kebab-case** (`local-storage-key`, not `localStorageKey`) +- Any PR with `feat!`, `fix!`, or `refactor!` must include an upgrade-changelog entry in `packages/web/CLAUDE.md` with grep targets and replacements + +## What NOT to Do + +- Don't add barrel `index.ts` exports +- Don't break semver without a conventional commit + `lerna publish` +- Don't duplicate `@leavittsoftware/lg-core-typescript` entity types in components +- Don't merge component API changes without updating `packages/web/CLAUDE.md` diff --git a/LICENSE.md b/LICENSE.md index 7b37c8fc9..6829b6067 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2025 Leavitt Software Solutions +Copyright 2026 Leavitt Software Solutions Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index fe90bd331..d046d56d4 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,14 @@ Types enable JavaScript developers to use highly-productive development tools an ### Create the leavittbook story -- [ ] Copy an existing demo/rename it/write your demo code -- [ ] Update my-app inside of leavittbook - - Add a route - - Add a menu item - - Add your component tag +See [`packages/leavittbook/CLAUDE.md`](packages/leavittbook/CLAUDE.md) for gallery conventions. + +- [ ] Copy an existing leavittbook demo (e.g. [`packages/leavittbook/src/demos/leavitt-error-page-demo.ts`](packages/leavittbook/src/demos/leavitt-error-page-demo.ts)) and rename it +- [ ] Update [`packages/leavittbook/src/my-app.ts`](packages/leavittbook/src/my-app.ts) + - Add a `page('/route', …)` handler + - Add a drawer `` (label must match `level1Text` on the demo header) + - Add a conditional render tag in `` +- [ ] Add `requires-auth` on `` if the demo calls api3 ### Important diff --git a/eslint.config.mjs b/eslint.config.mjs index 8d6daf296..f29d2dc39 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,7 @@ export default [ ecmaVersion: 2020, sourceType: 'module', parserOptions: { - project: true, + project: ['./tsconfig.json', './tsconfig.spec.json'], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/package.json b/package.json index ae2f10a40..e18b3840c 100644 --- a/package.json +++ b/package.json @@ -7,53 +7,53 @@ "packages/*" ], "dependencies": { - "@leavittsoftware/lg-core-typescript": "^5.1208.0", + "@leavittsoftware/lg-core-typescript": "^5.1342.0", "@material/web": "^2.4.1", "api-viewer-element": "^1.0.0-pre.10", - "lit": "3.3.2", + "countup.js": "^2.10.0", + "lit": "3.3.3", "page": "^1.11.6", "promise-parallel-throttle": "^3.5.0", "tslib": "^2.8.1" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.11.0", - "@rollup/plugin-babel": "^6.1.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "^10.0.1", "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-terser": "^0.4.4", - "@types/dom-navigation": "^1.0.7", + "@rollup/plugin-terser": "^1.0.0", + "@types/google.maps": "*", "@types/jasmine": "^6.0.0", "@types/page": "^1.11.9", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/eslint-plugin": "^8.62.0", + "@typescript-eslint/parser": "^8.62.0", "@web/dev-server": "^0.4.6", "@web/rollup-plugin-html": "^3.1.0", - "@web/rollup-plugin-import-meta-assets": "^2.3.2", - "concurrently": "^9.2.1", - "deepmerge": "^4.3.1", - "eslint": "^9.39.1", - "eslint-plugin-lit": "^2.2.1", + "@web/rollup-plugin-import-meta-assets": "^2.3.3", + "concurrently": "^10.0.3", + "eslint": "^10.5.0", + "eslint-plugin-lit": "^2.3.1", "eslint-plugin-wc": "^3.1.0", - "happy-dom": "^20.7.0", - "jasmine": "^6.1.0", - "jasmine-ts": "^0.4.0", - "lerna": "^9.0.4", + "jasmine": "^6.3.0", + "lerna": "^9.0.7", "lit-analyzer": "^2.0.3", - "patch-package": "^8.0.1", - "prettier": "^3.8.1", - "replace": "^1.2.2", + "prettier": "^3.8.4", "rimraf": "^6.1.3", - "rollup": "^4.59.0", - "rollup-plugin-copy": "^3.5.0", + "rollup": "^4.62.2", "rollup-plugin-workbox": "^8.1.3", - "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.56.1", + "typescript": "^6.0.3", "vscode-css-languageservice": "^6.3.10" }, "overrides": { "lit-analyzer": { "vscode-css-languageservice": "latest" - } + }, + "dompurify": "^3.4.11", + "js-yaml": "^4.2.0", + "rollup-plugin-workbox": { + "esbuild": "^0.28.1" + }, + "tar": "^7.5.17" }, "scripts": { "start": "concurrently --kill-others --names tsc,wds,cem \"tsc --watch\" \"web-dev-server\" \"npm run cem:watch\"", diff --git a/packages/leavittbook/CLAUDE.md b/packages/leavittbook/CLAUDE.md new file mode 100644 index 000000000..21e833493 --- /dev/null +++ b/packages/leavittbook/CLAUDE.md @@ -0,0 +1,87 @@ +# Leavittbook + +Local component gallery for `@leavittsoftware/web`. Not published to npm. + +## Purpose + +- Live demos of `titanium-*` and `leavitt-*` elements +- CEM-generated API docs via `api-viewer-element` and [`custom-elements.json`](../custom-elements.json) +- [`story-header`](src/shared/story-header.ts) for class/tag metadata + +Component APIs live in [`packages/web/CLAUDE.md`](../web/CLAUDE.md). + +## Adding a demo + +1. Copy an existing demo (e.g. [`src/demos/leavitt-error-page-demo.ts`](src/demos/leavitt-error-page-demo.ts)) → `src/demos/-demo.ts` +2. Register in [`src/my-app.ts`](src/my-app.ts): + - `page('/route', …)` lazy import + - `` in the drawer (`level1Text` must match the nav label) + - `${this.page === 'route-key' ? html`<…-demo>` : nothing}` in `` +3. Run `npm start` (CEM watch keeps `custom-elements.json` current) + +## Page layout + +``` +leavitt-app-main-content-container (.pendingStateElement=${this}) + main + leavitt-app-navigation-header (level1Text matches drawer label, level1Href="/route") + leavitt-app-width-limiter + story-header + …variants… + api-docs (src="./custom-elements.json" selected="element-tag") +``` + +- Add `leavitt-app-navigation-footer` **only** when it contains real actions (Save/Cancel, bulk actions, pagination). Do not add empty footers. +- Import shared typography from `@leavittsoftware/web/titanium/styles/styles` before local font rules. + +## Routing (intentional divergence from skeleton) + +Leavittbook **removes** inactive demo pages from the DOM on navigation (`${this.page === 'x' ? html`…` : nothing}`). Production apps scaffolded from skeleton keep pages mounted with `?hidden` + `.isActive`. The gallery resets demo state on each visit. + +Use `connectedCallback` / `disconnectedCallback` for per-visit setup and teardown. + +## Auth + +The gallery is public. Demos that call **api3** require Leavitt login: + +| Demo | Route | +| --------------------- | -------------------------------- | +| Company select | `/leavitt-company-select` | +| Person select | `/leavitt-person-select` | +| Person company select | `/leavitt-person-company-select` | +| Person group select | `/leavitt-person-group-select` | +| File explorer | `/leavitt-file-explorer` | +| Email history viewer | `/leavitt-email-history-viewer` | + +Drawer links for these demos show a trailing `passkey` icon. + +Pattern: + +- Gate demo content with `UserManager.identity` inline in `render()` — `${UserManager.identity ? html\`…\` : nothing}` (do not cache in demo state) +- `requires-auth` on `` — shows an **Authentication is required for this demo** filled tonal button when not authenticated; calls `UserManager.authenticate()` (redirect handles login) + +No Auth0 redirect until the user clicks that button. Shell uses `UserManager.initialize()` only (profile menu, feedback dialogs). No whole-app Auth0 gate. + +## Toolbar search + +After each route change, `my-app` sets `showSearch` from the active page’s `searchController` (see skeleton pattern). Demos that need the shared toolbar search expose `searchController` (usually via `TitaniumSiteSearchTextFieldController`). + +## Demo styling conventions + +- Pair `background` / accent tokens with matching `on-*` foreground tokens +- Use `labelStyles` for checkbox/radio/switch labels when present +- Minimum 13px font-size in component-local CSS +- `@property()` / `@state()` use `accessor` (Lit 3) +- `promiseTracking` + `PendingStateEvent` for async work; `ShowSnackbarEvent` for errors + +## Scaffolding sync (manual) + +When drift-checking against [skeleton.leavitt.com](https://github.com/LeavittSoftware/skeleton.leavitt.com) `develop`: + +| Skeleton | Leavittbook | +| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `theme.css`, `theme-custom.css` | `packages/leavittbook/` | +| `src/styles/my-app-styles.ts`, `form-styles.ts`, `hero-styles.ts`, `input-styles.ts`, `label-styles.ts`, `nice-badge-styles.ts` | `packages/leavittbook/src/styles/` (keep `story-styles.ts`) | +| `.editorconfig`, `.vscode/settings.json` | monorepo root | + +Do **not** copy skeleton’s `my-app.ts` routing model wholesale — see routing section above. diff --git a/packages/leavittbook/rollup.config.js b/packages/leavittbook/rollup.config.js index 089bd9ad8..429b3e8bc 100644 --- a/packages/leavittbook/rollup.config.js +++ b/packages/leavittbook/rollup.config.js @@ -1,11 +1,28 @@ +import { cpSync, existsSync, mkdirSync } from 'node:fs'; +import { basename, join } from 'node:path'; import nodeResolve from '@rollup/plugin-node-resolve'; -import copy from 'rollup-plugin-copy'; import terser from '@rollup/plugin-terser'; import { rollupPluginHTML as html } from '@web/rollup-plugin-html'; import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets'; import { generateSW } from 'rollup-plugin-workbox'; import path from 'path'; +/** Copies static assets into dist without pulling in deprecated glob/inflight via rollup-plugin-copy. */ +function copyStaticAssets(targets) { + return { + name: 'copy-static-assets', + writeBundle() { + for (const { src, dest } of targets) { + if (!existsSync(src)) { + continue; + } + mkdirSync(dest, { recursive: true }); + cpSync(src, join(dest, basename(src)), { recursive: true }); + } + }, + }; +} + export default { input: 'index.html', output: { @@ -29,6 +46,8 @@ export default { nodeResolve(), /** Minify HTML */ terser({ + // @rollup/plugin-terser v1's multi-worker pool segfaults on Node 22+; pin to a single worker. + maxWorkers: 1, ecma: 2022, module: true, warnings: true, @@ -58,17 +77,15 @@ export default { clientsClaim: true, runtimeCaching: [{ urlPattern: 'polyfills/*.js', handler: 'CacheFirst' }], }), - copy({ - targets: [ - { src: 'custom-elements.json', dest: 'dist' }, - { src: 'theme.css', dest: 'dist' }, - { src: 'theme-custom.css', dest: 'dist' }, - { src: 'manifest.json', dest: 'dist' }, - { src: 'manifest', dest: 'dist' }, - { src: 'fonts', dest: 'dist' }, - { src: 'images', dest: 'dist' }, - { src: '404.html', dest: 'dist' }, - ], - }), + copyStaticAssets([ + { src: 'custom-elements.json', dest: 'dist' }, + { src: 'theme.css', dest: 'dist' }, + { src: 'theme-custom.css', dest: 'dist' }, + { src: 'manifest.json', dest: 'dist' }, + { src: 'manifest', dest: 'dist' }, + { src: 'fonts', dest: 'dist' }, + { src: 'images', dest: 'dist' }, + { src: '404.html', dest: 'dist' }, + ]), ], }; diff --git a/packages/leavittbook/src/demos/leavitt-company-select-demo.ts b/packages/leavittbook/src/demos/leavitt-company-select-demo.ts index 37742175f..cca2a2424 100644 --- a/packages/leavittbook/src/demos/leavitt-company-select-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-company-select-demo.ts @@ -7,7 +7,7 @@ import '@api-viewer/docs'; import '@material/web/button/filled-tonal-button'; import '@leavittsoftware/web/leavitt/company-select/company-select'; -import { html, LitElement } from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { customElement, query, state } from 'lit/decorators.js'; import { LeavittCompanySelect } from '@leavittsoftware/web/leavitt/company-select/company-select'; import { DOMEvent } from '@leavittsoftware/web/titanium/types/dom-event'; @@ -17,12 +17,15 @@ import { ThemePreference } from '@leavittsoftware/web/leavitt/theme/theme-prefer import api3UserService from '../services/api3-user-service'; import StoryStyles from '../styles/story-styles'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-company-select-demo') export class LeavittCompanySelectDemo extends ThemePreference(LitElement) { @state() private accessor disableMenuOpenOnFocus: boolean = false; @query('leavitt-company-select[methods-demo]') protected accessor methodsSelect!: LeavittCompanySelect; + #auth = new AuthIdentityController(this); + static styles = [StoryStyles]; render() { @@ -32,13 +35,14 @@ export class LeavittCompanySelectDemo extends ThemePreference(LitElement) { - + + ${this.#auth.identity + ? html`

Filled

@@ -138,6 +143,10 @@ export class LeavittCompanySelectDemo extends ThemePreference(LitElement) { >
+ + ` + : nothing} +
diff --git a/packages/leavittbook/src/demos/leavitt-email-history-viewer-demo.ts b/packages/leavittbook/src/demos/leavitt-email-history-viewer-demo.ts index 9ad40b64c..3666b3092 100644 --- a/packages/leavittbook/src/demos/leavitt-email-history-viewer-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-email-history-viewer-demo.ts @@ -4,24 +4,31 @@ import '@leavittsoftware/web/leavitt/app/app-main-content-container'; import '@leavittsoftware/web/leavitt/app/app-navigation-header'; import '@leavittsoftware/web/leavitt/app/app-width-limiter'; import '@api-viewer/docs'; -import '@material/web/divider/divider'; -import '@leavittsoftware/web/leavitt/email-history-viewer/email-history-viewer'; import '@leavittsoftware/web/leavitt/email-history-viewer/email-history-viewer-filled'; -import { css, html, LitElement } from 'lit'; +import { css, html, LitElement, nothing } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import { ThemePreference } from '@leavittsoftware/web/leavitt/theme/theme-preference'; - -import LeavittEmailHistoryViewer from '@leavittsoftware/web/leavitt/email-history-viewer/email-history-viewer'; +import { TitaniumSiteSearchTextFieldController } from '@leavittsoftware/web/titanium/site-search-text-field-controller/site-search-text-field-controller'; import LeavittEmailHistoryViewerFilled from '@leavittsoftware/web/leavitt/email-history-viewer/email-history-viewer-filled'; + import StoryStyles from '../styles/story-styles'; import { siteSearchTextFieldContext } from '../contexts/site-search-text-field-context'; import api3UserService from '../services/api3-user-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-email-history-viewer-demo') export class LeavittEmailHistoryViewerDemo extends ThemePreference(LitElement) { - @query('leavitt-email-history-viewer') protected accessor demo1!: LeavittEmailHistoryViewer; - @query('leavitt-email-history-viewer-filled') protected accessor demo2!: LeavittEmailHistoryViewerFilled; + /** Always active while mounted — leavittbook removes inactive demos from the DOM. */ + isActive = true; + + @query('leavitt-email-history-viewer-filled') private accessor viewer!: LeavittEmailHistoryViewerFilled; + + #auth = new AuthIdentityController(this); + + get searchController(): TitaniumSiteSearchTextFieldController | null { + return this.viewer?.searchController ?? null; + } connectedCallback() { super.connectedCallback(); @@ -52,33 +59,24 @@ export class LeavittEmailHistoryViewerDemo extends ThemePreference(LitElement) { - + -
-

Filled

-
- -
-
+ ${this.#auth.identity + ? html` +
+
+ +
+
+ ` + : nothing} - - - - -
-

Outlined

-
- -
-
- -
diff --git a/packages/leavittbook/src/demos/leavitt-error-page-demo.ts b/packages/leavittbook/src/demos/leavitt-error-page-demo.ts index 18a24c34b..e4fa867f9 100644 --- a/packages/leavittbook/src/demos/leavitt-error-page-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-error-page-demo.ts @@ -42,7 +42,8 @@ export class LeavittErrorPageDemo extends LitElement { Return home or contact support.`} + .message=${html`The page you requested could not be found. Return home or + contact support.`} > diff --git a/packages/leavittbook/src/demos/leavitt-file-explorer-demo.ts b/packages/leavittbook/src/demos/leavitt-file-explorer-demo.ts index fa37ba311..c19f7b60e 100644 --- a/packages/leavittbook/src/demos/leavitt-file-explorer-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-file-explorer-demo.ts @@ -7,14 +7,17 @@ import '@api-viewer/docs'; import '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; import '@leavittsoftware/web/leavitt/file-explorer/file-explorer'; -import { html, LitElement } from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import StoryStyles from '../styles/story-styles'; import api3UserService from '../services/api3-user-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-file-explorer-demo') export class LeavittFileExplorerDemo extends LitElement { + #auth = new AuthIdentityController(this); + connectedCallback() { super.connectedCallback(); api3UserService.addHeader('X-LGAppName', 'FileExplorer'); @@ -36,13 +39,17 @@ export class LeavittFileExplorerDemo extends LitElement { - - -
-

File Explorer

-

File explorer component with API integration

- -
+ + + ${this.#auth.identity + ? html` +
+

File Explorer

+

File explorer component with API integration

+ +
+ ` + : nothing}
diff --git a/packages/leavittbook/src/demos/leavitt-person-company-select-demo.ts b/packages/leavittbook/src/demos/leavitt-person-company-select-demo.ts index b16f125f6..4d90b75aa 100644 --- a/packages/leavittbook/src/demos/leavitt-person-company-select-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-person-company-select-demo.ts @@ -8,17 +8,20 @@ import '@material/web/button/filled-tonal-button'; import '@leavittsoftware/web/leavitt/person-company-select/person-company-select'; import '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; -import { html, LitElement } from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import { LeavittPersonCompanySelect } from '@leavittsoftware/web/leavitt/person-company-select/person-company-select'; import StoryStyles from '../styles/story-styles'; import api3UserService from '../services/api3-user-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-person-company-select-demo') export class LeavittPersonCompanySelectDemo extends LitElement { @query('leavitt-person-company-select[methods-demo]') protected accessor methodsSelect!: LeavittPersonCompanySelect; + #auth = new AuthIdentityController(this); + static styles = [StoryStyles]; render() { @@ -29,8 +32,10 @@ export class LeavittPersonCompanySelectDemo extends LitElement { - + + ${this.#auth.identity + ? html`

Methods

@@ -50,20 +55,24 @@ export class LeavittPersonCompanySelectDemo extends LitElement { @@ -81,6 +90,10 @@ export class LeavittPersonCompanySelectDemo extends LitElement {
+ + ` + : nothing} +
diff --git a/packages/leavittbook/src/demos/leavitt-person-group-select-demo.ts b/packages/leavittbook/src/demos/leavitt-person-group-select-demo.ts index 2cecaffa7..f8c18bba8 100644 --- a/packages/leavittbook/src/demos/leavitt-person-group-select-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-person-group-select-demo.ts @@ -7,17 +7,20 @@ import '@api-viewer/docs'; import '@material/web/button/filled-tonal-button'; import '@leavittsoftware/web/leavitt/person-group-select/person-group-select'; -import { html, LitElement } from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import { LeavittPersonGroupSelect } from '@leavittsoftware/web/leavitt/person-group-select/person-group-select'; import StoryStyles from '../styles/story-styles'; import api3UserService from '../services/api3-user-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-person-group-select-demo') export class LeavittPersonGroupSelectDemo extends LitElement { @query('leavitt-person-group-select[methods-demo]') protected accessor methodsSelect!: LeavittPersonGroupSelect; + #auth = new AuthIdentityController(this); + static styles = [StoryStyles]; render() { @@ -28,8 +31,10 @@ export class LeavittPersonGroupSelectDemo extends LitElement { - + + ${this.#auth.identity + ? html`

Default

@@ -37,21 +42,25 @@ export class LeavittPersonGroupSelectDemo extends LitElement { @@ -77,6 +86,10 @@ export class LeavittPersonGroupSelectDemo extends LitElement {
+ + ` + : nothing} +
diff --git a/packages/leavittbook/src/demos/leavitt-person-select-demo.ts b/packages/leavittbook/src/demos/leavitt-person-select-demo.ts index 6ef124a1f..97c6a0922 100644 --- a/packages/leavittbook/src/demos/leavitt-person-select-demo.ts +++ b/packages/leavittbook/src/demos/leavitt-person-select-demo.ts @@ -8,18 +8,21 @@ import '@material/web/button/filled-tonal-button'; import '@leavittsoftware/web/leavitt/profile-picture/profile-picture'; import '@leavittsoftware/web/leavitt/person-select/person-select'; -import { html, LitElement } from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import { LeavittPersonSelect } from '@leavittsoftware/web/leavitt/person-select/person-select'; import { Person } from '@leavittsoftware/lg-core-typescript'; import StoryStyles from '../styles/story-styles'; import api3UserService from '../services/api3-user-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; @customElement('leavitt-person-select-demo') export class LeavittPersonSelectDemo extends LitElement { @query('leavitt-person-select[methods-demo]') protected accessor methodsSelect!: LeavittPersonSelect; + #auth = new AuthIdentityController(this); + static styles = [StoryStyles]; render() { @@ -29,8 +32,10 @@ export class LeavittPersonSelectDemo extends LitElement { - + + ${this.#auth.identity + ? html`

Methods

@@ -46,7 +51,6 @@ export class LeavittPersonSelectDemo extends LitElement { Local searching @@ -140,6 +145,10 @@ export class LeavittPersonSelectDemo extends LitElement {
+ + ` + : nothing} +
diff --git a/packages/leavittbook/src/demos/leavitt-user-feedback-demo.ts b/packages/leavittbook/src/demos/leavitt-user-feedback-demo.ts deleted file mode 100644 index 079cb54f6..000000000 --- a/packages/leavittbook/src/demos/leavitt-user-feedback-demo.ts +++ /dev/null @@ -1,57 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; -import '@material/web/button/filled-tonal-button'; -import '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; -import '@leavittsoftware/web/leavitt/user-feedback/user-feedback'; -import '@leavittsoftware/web/leavitt/user-feedback/report-a-problem-dialog'; -import '@leavittsoftware/web/leavitt/user-feedback/provide-feedback-dialog'; - -import { html, LitElement } from 'lit'; -import { customElement, query } from 'lit/decorators.js'; -import { ReportAProblemDialog } from '@leavittsoftware/web/leavitt/user-feedback/report-a-problem-dialog'; -import { ProvideFeedbackDialog } from '@leavittsoftware/web/leavitt/user-feedback/provide-feedback-dialog'; - -import UserManager from '../services/user-manager-service'; -import StoryStyles from '../styles/story-styles'; - -@customElement('leavitt-user-feedback-demo') -export class LeavittUserFeedbackDemo extends LitElement { - @query('report-a-problem-dialog') protected accessor reportAProblemDialog!: ReportAProblemDialog; - @query('provide-feedback-dialog') protected accessor provideFeedbackDialog!: ProvideFeedbackDialog; - - static styles = [StoryStyles]; - - render() { - return html` - -
- - - - - -
-

Default

-

User feedback components with report problem and provide feedback dialogs

- - this.reportAProblemDialog.show()}>Report a problem - this.provideFeedbackDialog.show()}>Provide feedback - - -
- - -
-
-
- - - - - `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-access-denied-page-demo.ts b/packages/leavittbook/src/demos/titanium-access-denied-page-demo.ts deleted file mode 100644 index 9e3c5c357..000000000 --- a/packages/leavittbook/src/demos/titanium-access-denied-page-demo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; - -import '@leavittsoftware/web/titanium/access-denied-page/access-denied-page'; - -import { html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-access-denied-page-demo') -export class TitaniumAccessDeniedPageDemo extends LitElement { - static styles = [StoryStyles]; - - render() { - return html` - -
- - - - -
-

Default

-

Access denied page sample

- -
- -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-address-input-demo.ts b/packages/leavittbook/src/demos/titanium-address-input-demo.ts index 1fa674abc..787970a0c 100644 --- a/packages/leavittbook/src/demos/titanium-address-input-demo.ts +++ b/packages/leavittbook/src/demos/titanium-address-input-demo.ts @@ -27,7 +27,7 @@ export class TitaniumAddressInputDemo extends LitElement { @query('titanium-address-input[demo-a]') protected accessor titaniumAddressInputDemoA!: TitaniumAddressInput; @query('titanium-address-input[demo-a-filled]') protected accessor titaniumAddressInputDemoAFilled!: TitaniumAddressInput; - @state() accessor allowInternational: boolean = false; + @state() protected accessor allowInternational: boolean = false; static styles = [ StoryStyles, @@ -69,7 +69,6 @@ export class TitaniumAddressInputDemo extends LitElement { ) => console.log('selected change 1', e.target.selected)} @@ -164,7 +163,6 @@ export class TitaniumAddressInputDemo extends LitElement { -
- - - -

Default

-

Basic card with default styling

- -

Lorem ipsum dolor sit amet

-
- -

Filled

-

Card with filled background

- -

Lorem ipsum dolor sit amet

-
- -

Elevated

-

Card with elevation shadow

- -

Lorem ipsum dolor sit amet

-
- -

With Image

-

Card containing an image

- - password -

Lorem ipsum dolor sit amet

-

-
- -

With Footer

-

Card with footer containing action buttons

- -

Lorem ipsum dolor sit amet

-
- - Save - -
- -

With Menu

-

Card with menu button in the header

- -

Lorem ipsum dolor sit amet

- more_vert -
-
- -

With List Items

-

Card containing interactive list items

- -

Lorem ipsum dolor sit amet

- more_vert -
- - -
Go Home
-
This will link you out in a new tab
- navigate_next -
- - -
Details
-
This will link you out in a new tab
- navigate_next -
-
-
- -
-
- - `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-chip-demo.ts b/packages/leavittbook/src/demos/titanium-chip-demo.ts index 73479625d..b1fa6211a 100644 --- a/packages/leavittbook/src/demos/titanium-chip-demo.ts +++ b/packages/leavittbook/src/demos/titanium-chip-demo.ts @@ -43,14 +43,14 @@ export class TitaniumChipDemo extends LitElement {

Various chip examples demonstrating different states and configurations

- + alert('click!')}> alert('click!')}> task_alt - alert('click!')}> + alert('click!')}> task_alt @@ -72,7 +72,7 @@ export class TitaniumChipDemo extends LitElement { - (e.target.selected = !e.target.selected)}> + (e.target.selected = !e.target.selected)}> @@ -84,14 +84,13 @@ export class TitaniumChipDemo extends LitElement { task_alt - alert('remove!')} @click=${() => alert('click!')}> + alert('remove!')} @click=${() => alert('click!')}> task_alt - - + diff --git a/packages/leavittbook/src/demos/titanium-chip-multi-select-demo.ts b/packages/leavittbook/src/demos/titanium-chip-multi-select-demo.ts index a6184585e..45b5dfb88 100644 --- a/packages/leavittbook/src/demos/titanium-chip-multi-select-demo.ts +++ b/packages/leavittbook/src/demos/titanium-chip-multi-select-demo.ts @@ -24,8 +24,7 @@ export class TitaniumChipMultiSelectDemo extends LitElement { @state() protected accessor demoItems: string[] = chipLabels.slice(0, 4); @state() protected accessor disabled: boolean = false; @state() protected accessor supportingText: string | null = 'Service animals are welcome.'; - @query('titanium-chip-multi-select') private accessor titaniumChipMultiSelect: TitaniumChipMultiSelect; - @query('titanium-chip-multi-select[filled]') private accessor titaniumChipMultiSelectFilled: TitaniumChipMultiSelect; + @query('titanium-chip-multi-select') private accessor titaniumChipMultiSelect!: TitaniumChipMultiSelect; static styles = [ StoryStyles, @@ -53,46 +52,12 @@ export class TitaniumChipMultiSelectDemo extends LitElement {
-

Default

- { - this.demoItems.push(chipLabels[this.demoItems.length % chipLabels.length]); - this.requestUpdate('demoItems'); - }} - >Add Animal add - ${repeat( - this.demoItems, - (o) => o, - (o, index) => - html` { - e.preventDefault(); - this.demoItems = this.demoItems.filter((_, i) => i !== index); - }} - >` - )} - -

Filled

- { this.titaniumChipMultiSelect.reportValidity(); - this.titaniumChipMultiSelectFilled.reportValidity(); }} >Report validity @@ -146,7 +110,6 @@ export class TitaniumChipMultiSelectDemo extends LitElement { { this.titaniumChipMultiSelect.reset(); - this.titaniumChipMultiSelectFilled.reset(); }} >Reset diff --git a/packages/leavittbook/src/demos/titanium-confirm-dialog-demo.ts b/packages/leavittbook/src/demos/titanium-confirm-dialog-demo.ts deleted file mode 100644 index d525b29b0..000000000 --- a/packages/leavittbook/src/demos/titanium-confirm-dialog-demo.ts +++ /dev/null @@ -1,72 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; -import '@material/web/icon/icon'; - -import '@material/web/button/filled-tonal-button'; - -import { css, html, LitElement } from 'lit'; -import { customElement, query, state } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/confirm-dialog/confirm-dialog'; -import { ConfirmDialogOpenEvent } from '@leavittsoftware/web/titanium/confirm-dialog/confirm-dialog-open-event'; - -import StoryStyles from '../styles/story-styles'; -import TitaniumConfirmDialog from '@leavittsoftware/web/titanium/confirm-dialog/confirm-dialog'; - -@customElement('titanium-confirm-dialog-demo') -export class TitaniumConfirmDialogDemo extends LitElement { - @state() private accessor confirmed = false; - @query('titanium-confirm-dialog') private accessor confirmDialog!: TitaniumConfirmDialog; - - async #open() { - const confirmationDialogEvent = new ConfirmDialogOpenEvent( - 'Confirm delete?', - 'Are you sure you would like to delete the universe? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Turpis massa tincidunt dui ut ornare. Ut tortor pretium viverra suspendisse potenti nullam. Dolor morbi non arcu risus. Porttitor rhoncus dolor purus non. Vitae justo eget magna fermentum iaculis eu non diam. Pretium quam vulputate dignissim suspendisse in est ante in. Semper quis lectus nulla at volutpat. Id volutpat lacus laoreet non curabitur gravida arcu ac tortor. Orci dapibus ultrices in iaculis.' - ); - this.dispatchEvent(confirmationDialogEvent); - this.confirmed = await confirmationDialogEvent.dialogResult; - } - - firstUpdated() { - this.addEventListener(ConfirmDialogOpenEvent.eventType, async (e: ConfirmDialogOpenEvent) => { - await import('@leavittsoftware/web/titanium/confirm-dialog/confirm-dialog'); - this.confirmDialog.handleEvent(e); - }); - } - - static styles = [ - StoryStyles, - css` - h3 { - margin-bottom: 12px; - } - `, - ]; - - render() { - return html` - -
- - - - - warning -

titanium-confirm-dialog is deprecated. Use titanium-confirmation-dialog instead.

-
-
-

Default

-

Confirmed: ${this.confirmed}

- Open -
- -
-
-
- - `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-confirmation-dialog-demo.ts b/packages/leavittbook/src/demos/titanium-confirmation-dialog-demo.ts index 700607ef8..c7e788f55 100644 --- a/packages/leavittbook/src/demos/titanium-confirmation-dialog-demo.ts +++ b/packages/leavittbook/src/demos/titanium-confirmation-dialog-demo.ts @@ -23,10 +23,10 @@ export class TitaniumConfirmationDialogDemo extends LitElement { @state() private accessor result4: 'confirmed' | 'cancel' | undefined; @state() private accessor favoriteSnack: string | undefined; - @query('titanium-confirmation-dialog[demo1]') private accessor confirmationDialog: TitaniumConfirmationDialog | null; - @query('titanium-confirmation-dialog[demo2]') private accessor confirmationDialog2: TitaniumConfirmationDialog | null; - @query('titanium-confirmation-dialog[demo3]') private accessor confirmationDialog3: TitaniumConfirmationDialog | null; - @query('titanium-confirmation-dialog[demo4]') private accessor confirmationDialog4: TitaniumConfirmationDialog | null; + @query('titanium-confirmation-dialog[demo1]') private accessor confirmationDialog!: TitaniumConfirmationDialog | null; + @query('titanium-confirmation-dialog[demo2]') private accessor confirmationDialog2!: TitaniumConfirmationDialog | null; + @query('titanium-confirmation-dialog[demo3]') private accessor confirmationDialog3!: TitaniumConfirmationDialog | null; + @query('titanium-confirmation-dialog[demo4]') private accessor confirmationDialog4!: TitaniumConfirmationDialog | null; static styles = [ StoryStyles, diff --git a/packages/leavittbook/src/demos/titanium-data-table-core-demo.ts b/packages/leavittbook/src/demos/titanium-data-table-core-demo.ts index 682f91037..0cd74d055 100644 --- a/packages/leavittbook/src/demos/titanium-data-table-core-demo.ts +++ b/packages/leavittbook/src/demos/titanium-data-table-core-demo.ts @@ -174,7 +174,7 @@ export class TitaniumDataTableCoreDemo extends LitElement { @state() private accessor sort: TitaniumDataTableCoreSortItem[] = []; @state() private accessor items: Array = this.sortItems(allTeslas, this.sort); @state() private accessor selected: Array = []; - @query('titanium-data-table-core') private accessor tableCore: TitaniumDataTableCore; + @query('titanium-data-table-core') private accessor tableCore!: TitaniumDataTableCore; /** * Sorts items based on multiple sort criteria * @param items Array of items to sort @@ -193,7 +193,7 @@ export class TitaniumDataTableCoreDemo extends LitElement { if (aValue == null) return sortItem.direction === 'asc' ? 1 : -1; if (bValue == null) return sortItem.direction === 'asc' ? -1 : 1; - let comparison = 0; + let comparison: number; // Handle string comparison if (typeof aValue === 'string' && typeof bValue === 'string') { @@ -394,7 +394,7 @@ export class TitaniumDataTableCoreDemo extends LitElement { @sort-changed=${async (e: DOMEvent>) => { this.sort = e.target.sort; const _delay = delay(300); - this.tableCore.loadWhile(_delay); + this.tableCore.trackLoadingPromise(_delay); await _delay; this.items = this.sortItems(this.items, this.sort); diff --git a/packages/leavittbook/src/demos/titanium-data-table-demo.ts b/packages/leavittbook/src/demos/titanium-data-table-demo.ts deleted file mode 100644 index 7c0e9e7e8..000000000 --- a/packages/leavittbook/src/demos/titanium-data-table-demo.ts +++ /dev/null @@ -1,530 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@material/web/divider/divider'; -import '@api-viewer/docs'; -import '@leavittsoftware/web/titanium/data-table/data-table-item'; -import '@leavittsoftware/web/titanium/data-table/data-table-header'; -import '@leavittsoftware/web/titanium/search-input/search-input'; -import '@material/web/dialog/dialog'; -import '@material/web/button/outlined-button'; -import '@material/web/button/filled-tonal-button'; -import '@material/web/icon/icon'; -import '@material/web/iconbutton/icon-button'; -import '@material/web/select/outlined-select.js'; -import '@material/web/select/select-option.js'; -import '@material/web/menu/menu'; -import '@material/web/menu/menu-item'; -import '@material/web/switch/switch'; -import '@material/web/chips/input-chip'; -import '@material/web/button/text-button'; - -import { css, html, LitElement } from 'lit'; -import { customElement, query, state } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/data-table/data-table'; -import { DOMEvent } from '@leavittsoftware/web/titanium/types/dom-event'; -import { Debouncer, getSearchTokens } from '@leavittsoftware/web/titanium/helpers/helpers'; -import { FilterController } from '@leavittsoftware/web/titanium/data-table/filter-controller'; -import { TitaniumDataTable } from '@leavittsoftware/web/titanium/data-table/data-table'; -import { TitaniumSearchInput } from '@leavittsoftware/web/titanium/search-input/search-input'; -import { repeat } from 'lit/directives/repeat.js'; -import { CloseMenuEvent, MdMenu, MenuItem } from '@material/web/menu/menu'; -import { MdIconButton } from '@material/web/iconbutton/icon-button'; -import { MdDialog } from '@material/web/dialog/dialog'; - -import StoryStyles from '../styles/story-styles'; - -type FilterKeys = 'Appearance'; -type Car = { Name: string; Appearance: 'Plaid' | 'Ugly' | 'Slick' }; - -const allTeslas: Array = [ - { Name: 'Model 3', Appearance: 'Slick' }, - { Name: 'Model X', Appearance: 'Slick' }, - { Name: 'Model Y', Appearance: 'Slick' }, - { Name: 'Model S', Appearance: 'Slick' }, - { Name: 'Cybertruck', Appearance: 'Ugly' }, - { Name: 'Tesla Semi', Appearance: 'Ugly' }, - { Name: 'Model X Plaid', Appearance: 'Plaid' }, - { Name: 'Model S Plaid', Appearance: 'Plaid' }, - { Name: 'Model S Plaid+', Appearance: 'Plaid' }, - { Name: 'Gen. 2 Roadster', Appearance: 'Slick' }, -]; - -@customElement('titanium-data-table-demo') -export class TitaniumDataTableDemo extends LitElement { - @state() protected accessor allItems: Array> = []; - @state() protected accessor items: Array> = []; - @state() protected accessor selected: Array> = []; - @state() protected accessor searchTerm: string = ''; - @state() protected accessor resultTotal: number = 0; - @state() protected accessor sortDirection: '' | 'asc' | 'desc' = 'asc'; - @state() protected accessor sortBy: string = 'Name'; - @state() protected accessor filterController: FilterController; - - @state() protected accessor singleSelect: boolean = false; - @state() protected accessor disableSelect: boolean = false; - @state() protected accessor disablePaging: boolean = false; - @state() protected accessor draggableItems: Array> = []; - - @query('titanium-data-table') protected accessor dataTable!: TitaniumDataTable; - @query('data-table-demo-filter-modal') protected accessor filterModal!: DataTableDemoFilterModal; - - constructor() { - super(); - this.filterController = new FilterController('/titanium-data-table'); - this.filterController.setFilter('Appearance', (val) => `BasketId eq ${val}`); - - this.filterController.subscribeToFilterChange(async () => { - if (this.dataTable) { - this.dataTable?.resetPage(); - this.#reload(); - } - }); - this.filterController.loadFromQueryString(); - } - - firstUpdated() { - this.#reset(); - this.items = this.allItems.slice(0); - this.draggableItems = [ - { Name: 'Model 3', Appearance: 'Slick' }, - { Name: 'Model X', Appearance: 'Slick' }, - { Name: 'Model Y', Appearance: 'Slick' }, - { Name: 'Model S', Appearance: 'Slick' }, - { Name: 'Cybertruck', Appearance: 'Ugly' }, - { Name: 'Tesla Semi', Appearance: 'Ugly' }, - { Name: 'Model X Plaid', Appearance: 'Plaid' }, - { Name: 'Model S Plaid', Appearance: 'Plaid' }, - { Name: 'Model S Plaid+', Appearance: 'Plaid' }, - { Name: 'Gen. 2 Roadster', Appearance: 'Slick' }, - ]; - } - - #reload() { - this.getItemsAsync(this.searchTerm); - } - - #reset() { - this.allItems = allTeslas.map((o) => ({ Name: o.Name, Appearance: o.Appearance })); - this.#reload(); - } - - #onSortDirectionChange(e: CustomEvent<'' | 'asc' | 'desc'>) { - this.sortDirection = e.detail; - this.dataTable.resetPage(); - this.#reload(); - } - - #onSortByChange(e: CustomEvent) { - this.sortBy = e.detail; - this.dataTable.resetPage(); - this.#reload(); - } - - #doSearchDebouncer = new Debouncer((searchTerm: string) => this.getItemsAsync(searchTerm)); - - async getItemsAsync(searchTerm: string) { - const searchTokens = getSearchTokens(searchTerm); - - const take = await this.dataTable.getTake(); - const page = await this.dataTable.getPage(); - const sortDirection = this.sortDirection === 'asc' ? 1 : -1; - - let filterItems = this.allItems.filter((o) => searchTokens.every((st) => o.Name?.trim().toLowerCase()?.includes(st.trim().toLowerCase()))); - - const appearanceValue = this.filterController.getValue('Appearance'); - if (appearanceValue) { - filterItems = filterItems.filter((o) => o.Appearance === appearanceValue); - } - - this.items = filterItems - .slice(0) - .sort((a, b) => (a[this.sortBy] === b[this.sortBy] ? 0 : (a[this.sortBy] ?? '') < (b[this.sortBy] ?? '') ? sortDirection : -sortDirection)) - .slice(take * page, take * page + take); - - this.resultTotal = filterItems.length; - } - - static styles = [ - StoryStyles, - css` - titanium-data-table { - margin: 24px 0 36px 0; - --titanium-page-control-select-width: 108px; - } - - knob-container { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 24px; - margin-top: 12px; - } - `, - ]; - - render() { - return html` - -
- - - - - warning -

titanium-data-table is deprecated. Use titanium-data-table-core instead (shown in separate demo).

-
- - -
-

Full working example

-

Table with items and method controls

- - >>) => { - this.selected = [...e.detail]; - }} - @paging-changed=${() => { - this.#reload(); - }} - narrow-max-width="800" - .count=${this.resultTotal} - .items=${this.items} - .searchTerm=${this.searchTerm} - ?single-select=${this.singleSelect} - ?disable-select=${this.disableSelect} - ?disable-paging=${this.disablePaging} - > - ) => { - this.searchTerm = e.target.value; - this.dataTable.resetPage(); - this.#doSearchDebouncer.debounce(this.searchTerm); - }} - > - - - - { - (e.detail.itemPath?.[0] as MenuItem & { action?: () => void })?.action?.(); - }} - > - this.#reset()}> - refresh - Refresh - - - - { - const car = allTeslas[this.allItems.length % allTeslas.length]; - const newItem: Partial = { Name: car.Name, Appearance: car.Appearance }; - this.allItems.push(newItem); - this.#reload(); - }} - > - add - Add item - - - - - { - this.filterModal.open(); - }} - > - filter_list - - - 1} - @click=${() => { - this.allItems = this.allItems.filter((f) => !this.selected.includes(f)); - this.resultTotal = this.resultTotal - this.selected.length; - this.#reload(); - }} - > - delete - - - - - - - - - ${repeat( - this.items ?? [], - (item) => item.Name, - (item) => html` - { - this.dataTable.clearSelection(); - }} - .item=${item} - slot="items" - > - ${item.Name ?? '-'} - ${item.Appearance ?? '-'} - ${item.Appearance ?? '-'} - - ` - )} - - -

Knobs

- - { - this.dataTable.clearSelection(); - this.singleSelect = !this.singleSelect; - }} - > - - - { - this.dataTable.clearSelection(); - this.disableSelect = !this.disableSelect; - }} - > - - - { - this.disablePaging = !this.disablePaging; - }} - > - - - this.dataTable.resetPage()}>Reset page - this.dataTable.clearSelection()}>Clear selection - -
- -
-

Draggable

-

Table with draggable items

- { - this.draggableItems = structuredClone(this.draggableItems); - await this.requestUpdate('draggableItems'); - }} - > - - - - - ${repeat( - this.draggableItems ?? [], - (item) => item.Name, - (item) => html` - - ${item.Name ?? '-'} - ${item.Appearance ?? '-'} - Learn More - - ` - )} - -

Results

-

${this.draggableItems.map((o) => o.Name).join(',')}

-
- - -
-
-
- `; - } -} - -@customElement('data-table-demo-filter-modal') -export class DataTableDemoFilterModal extends LitElement { - @state() protected accessor filterController: FilterController; - @state() protected accessor appearance: string; - - @query('md-dialog') private accessor dialog!: MdDialog; - - async firstUpdated() { - this.filterController.subscribeToFilterChange(async () => { - this.requestUpdate('filterController'); - }); - } - - public async open() { - this.dialog.show(); - } - - #preventDialogOverflow() { - const dialog = this.dialog.shadowRoot?.querySelector('dialog'); - const container = dialog?.querySelector('.container'); - const scroller = container?.querySelector('.scroller'); - if (scroller) { - scroller.style.overflow = 'initial'; - } - if (container) { - container.style.overflow = 'initial'; - } - } - - #restoreDialogOverflow() { - const dialog = this.dialog.shadowRoot?.querySelector('dialog'); - const container = dialog?.querySelector('.container'); - const scroller = container?.querySelector('.scroller'); - if (scroller) { - scroller.style.overflow = ''; - } - if (container) { - container.style.overflow = ''; - } - } - - static styles = [ - css` - :host { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - } - - md-dialog { - width: 100%; - } - - md-dialog form { - container-type: inline-size; - } - - md-outlined-select { - width: 100%; - } - - [hidden] { - display: none !important; - } - `, - ]; - - render() { - return html` - { - e.preventDefault(); - this.filterController.setValue('Appearance', null); - this.requestUpdate('filterController'); - }} - > - conditions - - - -
Filter items by
-
- this.#preventDialogOverflow()} - @closing=${() => this.#restoreDialogOverflow()} - label="Appearance" - hasLeadingIcon - .value=${this.filterController.getValue('Appearance') ?? ''} - @request-selection=${(e) => { - this.appearance = e.target.value; - }} - > - conditions - - -
Ugly
- conditions -
- -
Plaid
- conditions -
- -
Slick
- conditions -
-
-
-
- this.dialog.close('cancel')}> Close - { - this.filterController.setValue('Appearance', this.appearance || null); - this.requestUpdate('filterController'); - this.dialog.close(); - }} - >Apply -
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-data-table-header-demo.ts b/packages/leavittbook/src/demos/titanium-data-table-header-demo.ts deleted file mode 100644 index 4951eaf62..000000000 --- a/packages/leavittbook/src/demos/titanium-data-table-header-demo.ts +++ /dev/null @@ -1,63 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@material/web/divider/divider'; -import '@material/web/icon/icon'; -import '@api-viewer/docs'; - -import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/data-table/data-table-header'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-data-table-header-demo') -export class TitaniumDataTableHeaderDemo extends LitElement { - static styles = [ - StoryStyles, - css` - header-container { - display: flex; - flex-direction: row; - gap: 24px; - } - `, - ]; - - render() { - return html` - -
- - - - - - warning -

titanium-data-table-header is deprecated. Use titanium-data-table-core instead (shown in separate demo).

-
- - -
-

Table headers

-

Data table header components with different alignment and sizing options

- - - - - - - - - -
- - -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-data-table-item-demo.ts b/packages/leavittbook/src/demos/titanium-data-table-item-demo.ts deleted file mode 100644 index 2bd5bcecf..000000000 --- a/packages/leavittbook/src/demos/titanium-data-table-item-demo.ts +++ /dev/null @@ -1,77 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@material/web/divider/divider'; -import '@material/web/icon/icon'; -import '@api-viewer/docs'; -import '@material/web/button/filled-tonal-button'; - -import { css, html, LitElement } from 'lit'; -import { customElement, query } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/data-table/data-table-item'; -import { TitaniumDataTableItem } from '@leavittsoftware/web/titanium/data-table/data-table-item'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-data-table-item-demo') -export class TitaniumDataTableItemDemo extends LitElement { - @query('titanium-data-table-item[select-demo]') protected accessor selectItem!: TitaniumDataTableItem; - - static styles = [ - StoryStyles, - css` - section { - display: flex; - margin-top: 12px; - gap: 12px; - } - `, - ]; - - render() { - return html` - -
- - - - - - warning -

titanium-data-table-item is deprecated. Use titanium-data-table-core instead (shown in separate demo).

-
- - -
-

Default

-

Examples using disabled, closeable, and readonly attribute

-
- Default - Selected - Select disabled - -
-
- -
-

Methods

-

Select, Deselect, Toggle

-
- Item -
- this.selectItem.select()}>select() - this.selectItem.deselect()}>deselect() - this.selectItem.toggleSelected()}>toggleSelected() -
-
-
- - -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-date-input-demo.ts b/packages/leavittbook/src/demos/titanium-date-input-demo.ts index 75430ae87..33af92446 100644 --- a/packages/leavittbook/src/demos/titanium-date-input-demo.ts +++ b/packages/leavittbook/src/demos/titanium-date-input-demo.ts @@ -19,8 +19,8 @@ import StoryStyles from '../styles/story-styles'; @customElement('titanium-date-input-demo') export class TitaniumDateInputDemo extends LitElement { - @query('titanium-date-input') protected accessor input!: TitaniumDateInput; - @query('titanium-date-input[filled]') protected accessor filledInput!: TitaniumDateInput; + @query('titanium-date-input[required]') protected accessor input!: TitaniumDateInput; + @query('titanium-date-input:nth-of-type(2)') protected accessor secondInput!: TitaniumDateInput; static styles = [ StoryStyles, @@ -66,8 +66,7 @@ export class TitaniumDateInputDemo extends LitElement { > ) => console.log('change', e.target.value)} > @@ -76,14 +75,14 @@ export class TitaniumDateInputDemo extends LitElement { { this.input.reset(); - this.filledInput.reset(); + this.secondInput.reset(); }} >Reset { this.input.reportValidity(); - this.filledInput.reportValidity(); + this.secondInput.reportValidity(); }} >Report validity diff --git a/packages/leavittbook/src/demos/titanium-date-range-selector-demo.ts b/packages/leavittbook/src/demos/titanium-date-range-selector-demo.ts index 0f5acd6c4..e986842eb 100644 --- a/packages/leavittbook/src/demos/titanium-date-range-selector-demo.ts +++ b/packages/leavittbook/src/demos/titanium-date-range-selector-demo.ts @@ -101,7 +101,7 @@ export class TitaniumDateRangeSelectorDemo extends LitElement {

Filled

- +
diff --git a/packages/leavittbook/src/demos/titanium-drawer-demo.ts b/packages/leavittbook/src/demos/titanium-drawer-demo.ts index c21e5c2b0..185699cf8 100644 --- a/packages/leavittbook/src/demos/titanium-drawer-demo.ts +++ b/packages/leavittbook/src/demos/titanium-drawer-demo.ts @@ -22,8 +22,8 @@ import StoryStyles from '../styles/story-styles'; @customElement('titanium-drawer-demo') export class TitaniumDrawerDemo extends LitElement { - @query('titanium-drawer[one]') private accessor drawerOne: TitaniumDrawer; - @query('titanium-drawer[two]') private accessor drawerTwo: TitaniumDrawer; + @query('titanium-drawer[one]') private accessor drawerOne!: TitaniumDrawer; + @query('titanium-drawer[two]') private accessor drawerTwo!: TitaniumDrawer; @state() private accessor drawerTwoMode: 'inline' | 'flyover' = 'flyover'; @state() private accessor drawerTwoOpen: boolean = false; diff --git a/packages/leavittbook/src/demos/titanium-duration-input-demo.ts b/packages/leavittbook/src/demos/titanium-duration-input-demo.ts index 86c0ea746..4a410958a 100644 --- a/packages/leavittbook/src/demos/titanium-duration-input-demo.ts +++ b/packages/leavittbook/src/demos/titanium-duration-input-demo.ts @@ -3,19 +3,14 @@ import '../shared/story-header'; import '@leavittsoftware/web/leavitt/app/app-main-content-container'; import '@leavittsoftware/web/leavitt/app/app-navigation-header'; import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@material/web/divider/divider'; -import '@material/web/icon/icon'; import '@api-viewer/docs'; import '@material/web/button/filled-tonal-button'; import { css, html, LitElement } from 'lit'; import { customElement, query, state } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/duration-input/duration-input'; import '@leavittsoftware/web/titanium/duration-input/filled-duration-input'; -import '@leavittsoftware/web/titanium/duration-input/outlined-duration-input'; import { TitaniumFilledDurationInput } from '@leavittsoftware/web/titanium/duration-input/filled-duration-input'; -import { TitaniumOutlinedDurationInput } from '@leavittsoftware/web/titanium/duration-input/outlined-duration-input'; import { DOMEvent } from '@leavittsoftware/web/titanium/types/dom-event'; import dayjs from 'dayjs/esm'; import duration from 'dayjs/esm/plugin/duration'; @@ -27,16 +22,13 @@ dayjs.extend(duration); @customElement('titanium-duration-input-demo') export class TitaniumDurationInputDemo extends LitElement { @state() private accessor filledDuration: duration.Duration | null = dayjs.duration(14400); - @state() private accessor outlinedDuration: duration.Duration | null = dayjs.duration(14400); - @query('titanium-filled-duration-input') private accessor filledInput: TitaniumFilledDurationInput; - @query('titanium-outlined-duration-input') private accessor outlinedInput: TitaniumOutlinedDurationInput; + @query('titanium-filled-duration-input[required]') private accessor filledInput!: TitaniumFilledDurationInput; static styles = [ StoryStyles, css` - titanium-filled-duration-input, - titanium-outlined-duration-input { + titanium-filled-duration-input { width: 100%; margin-bottom: 24px; } @@ -50,13 +42,6 @@ export class TitaniumDurationInputDemo extends LitElement { - - warning -

- titanium-duration-input is deprecated. Use titanium-filled-duration-input or - titanium-outlined-duration-input instead (shown below). -

-
@@ -94,54 +79,14 @@ export class TitaniumDurationInputDemo extends LitElement {
- - - - - - -
-

Outlined duration input

- - ) => { - this.outlinedDuration = e.target.duration; - }} - > - -
- { - this.outlinedInput.reset(); - }} - >Reset - { - this.outlinedInput.focus(); - }} - >Focus - { - this.outlinedInput.reportValidity(); - }} - >Report validity -
-
-

With different durations

- - - + + +
- +
diff --git a/packages/leavittbook/src/demos/titanium-error-page-demo.ts b/packages/leavittbook/src/demos/titanium-error-page-demo.ts deleted file mode 100644 index b16c5852f..000000000 --- a/packages/leavittbook/src/demos/titanium-error-page-demo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; - -import '@leavittsoftware/web/titanium/error-page/error-page'; - -import { html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-error-page-demo') -export class TitaniumErrorPageDemo extends LitElement { - static styles = [StoryStyles]; - - render() { - return html` - -
- - - -
-

Default

- -

TemplateResult message

- Go back home or contact support.`} - > -
- -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-full-page-loading-indicator-demo.ts b/packages/leavittbook/src/demos/titanium-full-page-loading-indicator-demo.ts deleted file mode 100644 index 88ad6d7d3..000000000 --- a/packages/leavittbook/src/demos/titanium-full-page-loading-indicator-demo.ts +++ /dev/null @@ -1,61 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; - -import '@material/web/button/filled-tonal-button'; - -import { html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/full-page-loading-indicator/full-page-loading-indicator'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-full-page-loading-indicator-demo') -export class TitaniumFullPageLoadingIndicatorDemo extends LitElement { - static styles = [StoryStyles]; - - render() { - return html` - -
- - - - -
-

Demo

-

A promise driven pending-state-events loading scrim

- - { - e.preventDefault(); - const work = new Promise((r) => setTimeout(r, 50)); - const work2 = new Promise((r) => setTimeout(r, 3000)); - window.dispatchEvent( - new CustomEvent('pending-state', { - composed: true, - bubbles: true, - detail: { promise: work.then(() => console.log('Done 1')) }, - }) - ); - window.dispatchEvent( - new CustomEvent('pending-state', { - composed: true, - bubbles: true, - detail: { promise: work2.then(() => console.log('Done 2')) }, - }) - ); - }} - >Open loading veil for 2 seconds -
- -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-header-demo.ts b/packages/leavittbook/src/demos/titanium-header-demo.ts deleted file mode 100644 index c75f76520..000000000 --- a/packages/leavittbook/src/demos/titanium-header-demo.ts +++ /dev/null @@ -1,83 +0,0 @@ -import '../shared/story-header'; - -import '@leavittsoftware/web/leavitt/app/app-main-content-container'; -import '@leavittsoftware/web/leavitt/app/app-navigation-header'; -import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@api-viewer/docs'; -import '@material/web/icon/icon'; - -import '@leavittsoftware/web/titanium/header/header'; - -import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -import StoryStyles from '../styles/story-styles'; - -@customElement('titanium-header-demo') -export class TitaniumHeaderDemo extends LitElement { - static styles = [ - StoryStyles, - css` - h1 { - margin-bottom: 24px; - } - `, - ]; - - render() { - return html` - -
- - - - - warning -

titanium-header is deprecated. Use leavitt-app-navigation-header instead.

-
-
-

No nav

- -
- -
-

Back Arrow (default window.history.back)

- -
- -
-

Back Arrow (overloaded action)

- { - alert('back clicked'); - }} - > -
- -
-

With custom icon

- { - alert('back clicked'); - }} - > -
- -
-

Custom icon with no navigation controls

- -
- - -
-
-
- `; - } -} diff --git a/packages/leavittbook/src/demos/titanium-icon-picker-demo.ts b/packages/leavittbook/src/demos/titanium-icon-picker-demo.ts index 967c92f3a..1d46dd4c3 100644 --- a/packages/leavittbook/src/demos/titanium-icon-picker-demo.ts +++ b/packages/leavittbook/src/demos/titanium-icon-picker-demo.ts @@ -16,7 +16,7 @@ import StoryStyles from '../styles/story-styles'; @customElement('titanium-icon-picker-demo') export class TitaniumIconPickerDemo extends LitElement { - @query('titanium-icon-picker[demo2]') private accessor requiredInput: TitaniumIconPicker; + @query('titanium-icon-picker[demo2]') private accessor requiredInput!: TitaniumIconPicker; static styles = [StoryStyles]; @@ -37,7 +37,7 @@ export class TitaniumIconPickerDemo extends LitElement {

Filled

Filled icon picker example

- +
diff --git a/packages/leavittbook/src/demos/titanium-input-validator-demo.ts b/packages/leavittbook/src/demos/titanium-input-validator-demo.ts index e86fc4751..68ea538b4 100644 --- a/packages/leavittbook/src/demos/titanium-input-validator-demo.ts +++ b/packages/leavittbook/src/demos/titanium-input-validator-demo.ts @@ -3,20 +3,16 @@ import '../shared/story-header'; import '@leavittsoftware/web/leavitt/app/app-main-content-container'; import '@leavittsoftware/web/leavitt/app/app-navigation-header'; import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@leavittsoftware/web/leavitt/profile-picture/profile-picture'; import '@material/web/iconbutton/outlined-icon-button'; import '@material/web/icon/icon'; import '@material/web/button/filled-tonal-button'; -import '@material/web/divider/divider'; import '@api-viewer/docs'; import '@leavittsoftware/web/titanium/input-validator/filled-input-validator'; -import '@leavittsoftware/web/titanium/input-validator/outlined-input-validator'; import { css, html, LitElement } from 'lit'; import { customElement, queryAll, state } from 'lit/decorators.js'; import { ShowSnackbarEvent } from '@leavittsoftware/web/titanium/snackbar/show-snackbar-event'; -import { TitaniumOutlinedInputValidator } from '@leavittsoftware/web/titanium/input-validator/outlined-input-validator'; import { TitaniumFilledInputValidator } from '@leavittsoftware/web/titanium/input-validator/filled-input-validator'; import StoryStyles from '../styles/story-styles'; @@ -24,29 +20,19 @@ import StoryStyles from '../styles/story-styles'; @customElement('titanium-input-validator-demo') export class TitaniumInputValidatorDemo extends LitElement { @state() private accessor filledIconSelected = ''; - @state() private accessor outlinedIconSelected = ''; - @queryAll('titanium-filled-input-validator') private accessor filledValidators: NodeListOf; - @queryAll('titanium-outlined-input-validator') private accessor outlinedValidators: NodeListOf; + @queryAll('titanium-filled-input-validator') private accessor filledValidators!: NodeListOf; static styles = [ StoryStyles, css` - md-divider { - margin-bottom: 48px; - } - button-container { padding: 12px; display: flex; gap: 12px; } - titanium-filled-input-validator, - titanium-outlined-input-validator { - width: 299px; - } - titanium-filled-input-validator { + width: 299px; --md-filled-field-container-shape: 16px; --md-filled-field-active-indicator-height: 0; @@ -70,13 +56,6 @@ export class TitaniumInputValidatorDemo extends LitElement { - - warning -

- titanium-input-validator is deprecated. Use titanium-outlined-input-validator or - titanium-filled-input-validator instead. -

-

Filled

- - - -
-

Outlined

- this.outlinedIconSelected === 'cruelty_free'} - .errorText=${'Bunny is not selected'} - > - - (this.outlinedIconSelected = 'cruelty_free')} - toggle - ?selected=${this.outlinedIconSelected === 'cruelty_free'} - > - cruelty_free - - (this.outlinedIconSelected = 'pets')} - toggle - ?selected=${this.outlinedIconSelected === 'pets'} - > - pets - - (this.outlinedIconSelected = 'person')} - toggle - ?selected=${this.outlinedIconSelected === 'person'} - > - person - - - -
- Array.from(this.outlinedValidators).forEach((v) => v.reportValidity())} - >Report Validity - - this.dispatchEvent(new ShowSnackbarEvent(`Check Validity is ${Array.from(this.outlinedValidators).map((v) => v.checkValidity())}`))} - > - Check Validity - Array.from(this.outlinedValidators).forEach((v) => v.reset())}> Reset -
-
- - diff --git a/packages/leavittbook/src/demos/titanium-page-control-demo.ts b/packages/leavittbook/src/demos/titanium-page-control-demo.ts index 40edfe479..ba347aaba 100644 --- a/packages/leavittbook/src/demos/titanium-page-control-demo.ts +++ b/packages/leavittbook/src/demos/titanium-page-control-demo.ts @@ -15,7 +15,7 @@ import StoryStyles from '../styles/story-styles'; @customElement('titanium-page-control-demo') export class TitaniumPageControlDemo extends LitElement { - @query('titanium-page-control[main]') private accessor pageControl: TitaniumPageControl; + @query('titanium-page-control[main]') private accessor pageControl!: TitaniumPageControl; @state() protected accessor count: number = 25; @state() protected accessor data; @state() protected accessor filteredData; @@ -70,7 +70,7 @@ export class TitaniumPageControlDemo extends LitElement {

Filled

Filled page control variant

- +
diff --git a/packages/leavittbook/src/demos/titanium-profile-picture-stack-demo.ts b/packages/leavittbook/src/demos/titanium-profile-picture-stack-demo.ts index d13f9d54b..9d4e08cf8 100644 --- a/packages/leavittbook/src/demos/titanium-profile-picture-stack-demo.ts +++ b/packages/leavittbook/src/demos/titanium-profile-picture-stack-demo.ts @@ -30,8 +30,8 @@ const randomPerson: Partial = { Id: 771130, FullName: 'Random Person', P @customElement('titanium-profile-picture-stack-demo') export class TitaniumProfilePictureStackDemo extends LitElement { - @state() people: Array> = [randomPerson, kaseyPerson, aaronPerson, randomPerson, randomPerson]; - @state() manyPeople: Array> = new Array(20).fill(kaseyPerson); + @state() private accessor people: Array> = [randomPerson, kaseyPerson, aaronPerson, randomPerson, randomPerson]; + @state() private accessor manyPeople: Array> = new Array(20).fill(kaseyPerson); static styles = [ StoryStyles, diff --git a/packages/leavittbook/src/demos/titanium-search-input-demo.ts b/packages/leavittbook/src/demos/titanium-search-input-demo.ts index 9e9059582..8d8cd3325 100644 --- a/packages/leavittbook/src/demos/titanium-search-input-demo.ts +++ b/packages/leavittbook/src/demos/titanium-search-input-demo.ts @@ -3,7 +3,6 @@ import '../shared/story-header'; import '@leavittsoftware/web/leavitt/app/app-main-content-container'; import '@leavittsoftware/web/leavitt/app/app-navigation-header'; import '@leavittsoftware/web/leavitt/app/app-width-limiter'; -import '@material/web/divider/divider'; import '@api-viewer/docs'; import '@material/web/button/filled-tonal-button'; @@ -11,15 +10,16 @@ import '@material/web/icon/icon'; import { html, LitElement } from 'lit'; import { customElement, query } from 'lit/decorators.js'; -import '@leavittsoftware/web/titanium/search-input/search-input'; -import { TitaniumSearchInput } from '@leavittsoftware/web/titanium/search-input/search-input'; +import '@leavittsoftware/web/titanium/search-input/filled-search-input'; +import TitaniumFilledSearchInput from '@leavittsoftware/web/titanium/search-input/filled-search-input'; import { DOMEvent } from '@leavittsoftware/web/titanium/types/dom-event'; +import { MdFilledTextField } from '@material/web/textfield/filled-text-field'; import StoryStyles from '../styles/story-styles'; @customElement('titanium-search-input-demo') export class TitaniumSearchInputDemo extends LitElement { - @query('titanium-search-input[method-focused]') protected accessor methodFocus!: TitaniumSearchInput; + @query('titanium-filled-search-input[method-focused]') protected accessor methodFocus!: TitaniumFilledSearchInput; static styles = [StoryStyles]; @@ -30,48 +30,46 @@ export class TitaniumSearchInputDemo extends LitElement { - - warning -

titanium-search-input is deprecated and no longer in use.

-
- +

Default

-

Basic search input with expand/collapse functionality

- ) => console.log(e.target.value)}> +

Basic filled search input with clear button

+ ) => console.log(e.target.value)}>

Disabled

Disabled search input

- +

Placeholder text

-

Search input with placeholder and hidden clear button

- +

Search input with custom placeholder

+
-

Collapse Prevented

-

Search input that stays expanded

- +

With value

+

Search input with an initial value

+

Methods

-

Demonstrates public methods

- +

Demonstrates focusing the underlying text field

+
- this.methodFocus.focus()}>Focus + this.methodFocus.shadowRoot?.querySelector('md-filled-text-field')?.focus()} + >Focus
- +
diff --git a/packages/leavittbook/src/demos/titanium-show-hide-demo.ts b/packages/leavittbook/src/demos/titanium-show-hide-demo.ts index f31a69e87..a7743c683 100644 --- a/packages/leavittbook/src/demos/titanium-show-hide-demo.ts +++ b/packages/leavittbook/src/demos/titanium-show-hide-demo.ts @@ -345,7 +345,7 @@ export class TitaniumShowHideDemo extends LitElement {

Filled button example

Read some text

(this.verticalStepValue = event.target.value)} .value=${this.verticalStepValue}> - +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ipsum arcu, semper ac aliquet eu, porttitor vel turpis. Nullam non dolor ac massa pharetra vulputate vel ac libero. In hac habitasse platea dictumst. Praesent lacus mi, vehicula eu euismod sit amet, accumsan porta diff --git a/packages/leavittbook/src/demos/titanium-smart-attachment-input-demo.ts b/packages/leavittbook/src/demos/titanium-smart-attachment-input-demo.ts index 22cac52df..b6cb17699 100644 --- a/packages/leavittbook/src/demos/titanium-smart-attachment-input-demo.ts +++ b/packages/leavittbook/src/demos/titanium-smart-attachment-input-demo.ts @@ -70,7 +70,7 @@ export class TitaniumSmartAttachmentInputDemo extends LitElement { 'xbm', ]; - @query('titanium-smart-attachment-input[filled]') private accessor smartAttachment!: TitaniumSmartAttachmentInput; + @query('titanium-smart-attachment-input[required]') private accessor smartAttachment!: TitaniumSmartAttachmentInput; @query('md-dialog') private accessor dialog!: any; static styles = [ @@ -112,9 +112,9 @@ export class TitaniumSmartAttachmentInputDemo extends LitElement {

-

Filled

+

With options

- - warning -

titanium-youtube-input is deprecated. Use titanium-filled-youtube-input instead (shown below).

-
@@ -81,56 +72,13 @@ export class TitaniumYoutubeInputDemo extends LitElement {
- - - - - - -
-

Default

-

YouTube video input with URL validation

- -
-

Disabled

Disabled YouTube input

- -
- -
-

Methods

-

Demonstrates public methods like reset and reportValidity

- ) => console.log(e.target.value)} - > -
-
- { - this.outlinedInput.reset(); - }} - >Reset - { - this.outlinedInput.focus(); - }} - >Focus - { - this.outlinedInput.reportValidity(); - }} - >Report validity -
+
- +
diff --git a/packages/leavittbook/src/getting-started.ts b/packages/leavittbook/src/getting-started.ts index f4a0fb911..cad43c5a7 100644 --- a/packages/leavittbook/src/getting-started.ts +++ b/packages/leavittbook/src/getting-started.ts @@ -73,9 +73,9 @@ export default class GettingStarted extends LitElement {

NPM install:

npm i @leavittsoftware/web

Include the element on your page.

- import '@leavittsoftware/web/titanium/card/card'; + import '@leavittsoftware/web/titanium/chip/chip';

Use the element:

- ${''} + ${''}
diff --git a/packages/leavittbook/src/my-app.ts b/packages/leavittbook/src/my-app.ts index 09b16ae67..017c09c71 100644 --- a/packages/leavittbook/src/my-app.ts +++ b/packages/leavittbook/src/my-app.ts @@ -3,7 +3,6 @@ import '@leavittsoftware/web/leavitt/app/app-logo'; import '@leavittsoftware/web/titanium/search-input/filled-search-input'; import '@leavittsoftware/web/titanium/toolbar/toolbar'; import '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; -import '@leavittsoftware/web/titanium/error-page/error-page'; import '@leavittsoftware/web/titanium/drawer/drawer'; import '@leavittsoftware/web/leavitt/profile-picture/profile-picture-menu'; import '@leavittsoftware/web/leavitt/user-feedback/report-a-problem-dialog'; @@ -36,6 +35,7 @@ import { mainMenuPositionContext } from '@leavittsoftware/web/leavitt/app/contex import { provide } from '@lit/context'; import { siteSearchTextFieldContext } from './contexts/site-search-text-field-context'; import { MdFilledTextField } from '@material/web/textfield/filled-text-field'; +import { PageElement } from '@leavittsoftware/web/titanium/site-search-text-field-controller/site-search-text-field-controller'; @customElement('my-app') export class MyApp extends PendingStateCatcher(LitElement) { @@ -48,7 +48,9 @@ export class MyApp extends PendingStateCatcher(LitElement) { @property({ type: String, reflect: true, attribute: 'main-menu-position' }) private mainMenuPosition: 'slim' | 'full' | 'drawer' = 'full'; - @query('titanium-drawer') private accessor drawer: TitaniumDrawer; + @query('titanium-drawer') private accessor drawer!: TitaniumDrawer; + + #resizeObserver: ResizeObserver | null = null; async connectedCallback() { super.connectedCallback(); @@ -56,7 +58,7 @@ export class MyApp extends PendingStateCatcher(LitElement) { UserManager.initialize(); } catch (error) { console.error(error); - this.fatalErrorMessage = error; + this.fatalErrorMessage = error instanceof Error ? error.message : String(error); this.#changePage('error'); return; } @@ -98,38 +100,43 @@ export class MyApp extends PendingStateCatcher(LitElement) { this.#applyTheme(); }); - const resizeObserver = new ResizeObserver((entries) => { + this.#resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentRect.width; if (width < 600) { + this.drawer.mode = 'flyover'; this.mainMenuPosition = 'drawer'; } else if (width >= 600 && width < 920) { + this.drawer.mode = 'inline'; + this.drawer.open(); this.mainMenuPosition = 'slim'; - this.drawer?.closeQuick(); } else { + this.drawer.mode = 'inline'; + this.drawer.open(); this.mainMenuPosition = this.prefersCollapsedMenu ? 'slim' : 'full'; - this.drawer?.closeQuick(); } } }); - resizeObserver.observe(this); + this.#resizeObserver.observe(this); - this.addEventListener(ChangePathEvent.eventName, (event: ChangePathEvent) => { + this.addEventListener(ChangePathEvent.eventName, ((event: ChangePathEvent) => { page.show(event.detail.path); - }); + }) as EventListener); - this.addEventListener(RedirectPathEvent.eventName, (event: RedirectPathEvent) => { + this.addEventListener(RedirectPathEvent.eventName, ((event: RedirectPathEvent) => { page.redirect(event.detail.path); - }); + }) as EventListener); - this.addEventListener(SiteErrorEvent.eventName, (event: SiteErrorEvent) => { + this.addEventListener(SiteErrorEvent.eventName, ((event: SiteErrorEvent) => { this.fatalErrorMessage = event.detail; this.#changePage('error'); - }); + }) as EventListener); page('*', (_ctx, next) => { - this.drawer?.close(); + if (this.drawer?.mode === 'flyover') { + this.drawer.close(); + } next(); }); @@ -137,16 +144,12 @@ export class MyApp extends PendingStateCatcher(LitElement) { page.show('/getting-started'); }); page('/getting-started', () => this.#changePage('getting-started', () => import('./getting-started.js'))); - page('/titanium-full-page-loading-indicator', () => - this.#changePage('titanium-full-page-loading-indicator', () => import('./demos/titanium-full-page-loading-indicator-demo.js')) - ); page('/available-cdn-icons', () => this.#changePage('available-cdn-icons', () => import('./demos/available-cdn-icons-demo.js'))); page('/leavitt-company-select', () => this.#changePage('leavitt-company-select', () => import('./demos/leavitt-company-select-demo.js'))); page('/leavitt-file-explorer', () => this.#changePage('leavitt-file-explorer', () => import('./demos/leavitt-file-explorer-demo.js'))); page('/titanium-date-range-selector', () => this.#changePage('titanium-date-range-selector', () => import('./demos/titanium-date-range-selector-demo.js'))); - page('/titanium-data-table-item', () => this.#changePage('titanium-data-table-item', () => import('./demos/titanium-data-table-item-demo.js'))); page('/leavitt-person-select', () => this.#changePage('leavitt-person-select', () => import('./demos/leavitt-person-select-demo.js'))); page('/leavitt-person-company-select', () => @@ -156,32 +159,18 @@ export class MyApp extends PendingStateCatcher(LitElement) { page('/leavitt-email-history-viewer', () => this.#changePage('leavitt-email-history-viewer', () => import('./demos/leavitt-email-history-viewer-demo.js'))); - page('/leavitt-user-feedback', () => this.#changePage('leavitt-user-feedback', () => import('./demos/leavitt-user-feedback-demo.js'))); page('/leavitt-error-page', () => this.#changePage('leavitt-error-page', () => import('./demos/leavitt-error-page-demo.js'))); page('/profile-picture', () => this.#changePage('profile-picture', () => import('./demos/profile-picture-demo.js'))); page('/profile-picture-menu', () => this.#changePage('profile-picture-menu', () => import('./demos/profile-picture-menu-demo.js'))); - page('/titanium-access-denied-page', () => this.#changePage('titanium-access-denied-page', () => import('./demos/titanium-access-denied-page-demo.js'))); - page('/titanium-data-table', () => this.#changePage('titanium-data-table', () => import('./demos/titanium-data-table-demo.js'))); - page('/titanium-data-table-item', () => this.#changePage('titanium-data-table-item', () => import('./demos/titanium-data-table-item-demo.js'))); + page('/titanium-data-table-core', () => this.#changePage('titanium-data-table-core', () => import('./demos/titanium-data-table-core-demo.js'))); page('/titanium-drawer', () => this.#changePage('titanium-drawer', () => import('./demos/titanium-drawer-demo.js'))); - page('/titanium-error-page', () => this.#changePage('titanium-error-page', () => import('./demos/titanium-error-page-demo.js'))); page('/titanium-address-input', () => this.#changePage('titanium-address-input', () => import('./demos/titanium-address-input-demo.js'))); - page('/titanium-header', () => this.#changePage('titanium-header', () => import('./demos/titanium-header-demo.js'))); page('/titanium-icon-picker', () => this.#changePage('titanium-icon-picker', () => import('./demos/titanium-icon-picker-demo.js'))); - page('/titanium-header', () => this.#changePage('titanium-header', () => import('./demos/titanium-header-demo.js'))); page('/titanium-chip-multi-select', () => this.#changePage('titanium-chip-multi-select', () => import('./demos/titanium-chip-multi-select-demo.js'))); page('/titanium-input-validator', () => this.#changePage('titanium-input-validator', () => import('./demos/titanium-input-validator-demo.js'))); - page('/titanium-data-table-header', () => this.#changePage('titanium-data-table-header', () => import('./demos/titanium-data-table-header-demo.js'))); - page('/titanium-data-table-core', () => this.#changePage('titanium-data-table-core', () => import('./demos/titanium-data-table-core-demo.js'))); - page('/titanium-promise-tracking', () => - this.#changePage('titanium-promise-tracking', () => import('./demos/titanium-promise-tracking-demo.js')) - ); - - page('/titanium-full-page-loading-indicator', () => - this.#changePage('titanium-full-page-loading-indicator', () => import('./demos/titanium-full-page-loading-indicator-demo.js')) - ); + page('/titanium-promise-tracking', () => this.#changePage('titanium-promise-tracking', () => import('./demos/titanium-promise-tracking-demo.js'))); page('/titanium-page-control', () => this.#changePage('titanium-page-control', () => import('./demos/titanium-page-control-demo.js'))); @@ -196,7 +185,6 @@ export class MyApp extends PendingStateCatcher(LitElement) { page('/titanium-styles', () => this.#changePage('titanium-styles', () => import('./demos/titanium-styles-demo.js'))); page('/titanium-snackbar', () => this.#changePage('titanium-snackbar', () => import('./demos/titanium-snackbar-demo.js'))); - page('/titanium-card', () => this.#changePage('titanium-card', () => import('./demos/titanium-card-demo.js'))); page('/titanium-chip', () => this.#changePage('titanium-chip', () => import('./demos/titanium-chip-demo.js'))); page('/titanium-youtube-input', () => this.#changePage('titanium-youtube-input', () => import('./demos/titanium-youtube-input-demo.js'))); page('/titanium-show-hide', () => this.#changePage('titanium-show-hide', () => import('./demos/titanium-show-hide-demo.js'))); @@ -205,7 +193,6 @@ export class MyApp extends PendingStateCatcher(LitElement) { this.#changePage('titanium-profile-picture-stack', () => import('./demos/titanium-profile-picture-stack-demo.js')) ); - page('/titanium-confirm-dialog', () => this.#changePage('titanium-confirm-dialog', () => import('./demos/titanium-confirm-dialog-demo.js'))); page('/titanium-confirmation-dialog', () => this.#changePage('titanium-confirmation-dialog', () => import('./demos/titanium-confirmation-dialog-demo.js'))); page('*', () => { @@ -215,6 +202,15 @@ export class MyApp extends PendingStateCatcher(LitElement) { page.start(); } + async disconnectedCallback() { + await super.disconnectedCallback(); + this.#resizeObserver?.disconnect(); + } + + #getActivePageElement(mainPage: string): PageElement | null { + return this.shadowRoot?.querySelector(`${mainPage}-demo`) ?? this.shadowRoot?.querySelector(mainPage) ?? null; + } + async #changePage(mainPage: string, importFunction?: () => Promise) { this.page = mainPage; try { @@ -223,11 +219,12 @@ export class MyApp extends PendingStateCatcher(LitElement) { this.dispatchEvent(new PendingStateEvent(importElements)); } await importElements; + await this.updateComplete; - this.showSearch = mainPage === 'leavitt-email-history-viewer'; + this.showSearch = !!this.#getActivePageElement(mainPage)?.searchController; } catch (error) { console.warn(error); - this.#showErrorPage(error); + this.#showErrorPage(error instanceof Error ? error.message : String(error)); } } @@ -296,8 +293,7 @@ export class MyApp extends PendingStateCatcher(LitElement) { ]; render() { - return html` - + return html` - +
@@ -359,18 +355,10 @@ export class MyApp extends PendingStateCatcher(LitElement) {

Titanium

- - block Access denied page - - location_on Address input - - dashboard Card - - label Chip @@ -379,30 +367,14 @@ export class MyApp extends PendingStateCatcher(LitElement) { checklist Chip multi select - - help_outline Confirm dialog - - check_circle Confirmation dialog - - table_chart Data table - - table_rows Data table core - - view_column Data table header - - - - format_list_numbered Data table item - - calendar_today Date input @@ -419,18 +391,6 @@ export class MyApp extends PendingStateCatcher(LitElement) { timer Duration input - - error Error page - - - - hourglass_top Full page loading indicator - - - - title Header - - emoji_symbols Icon picker @@ -484,10 +444,12 @@ export class MyApp extends PendingStateCatcher(LitElement) {

Leavitt

business Company select + passkey mail Email history viewer + passkey @@ -496,18 +458,22 @@ export class MyApp extends PendingStateCatcher(LitElement) { folder_open File explorer + passkey badge Person company select + passkey diversity_3 Person group select + passkey person_search Person select + passkey @@ -517,10 +483,6 @@ export class MyApp extends PendingStateCatcher(LitElement) { account_box Profile picture menu - - - feedback User feedback -
Oops, something went wrong.
` : nothing} - ${this.page === 'available-cdn-icons' - ? html` ` - : nothing} - ${this.page === 'titanium-date-range-selector' - ? html` ` - : nothing} - ${this.page === 'leavitt-person-select' - ? html` ` - : nothing} - ${this.page === 'leavitt-company-select' - ? html` ` - : nothing} - ${this.page === 'leavitt-email-history-viewer' - ? html` ` - : nothing} - ${this.page === 'leavitt-file-explorer' - ? html` ` - : nothing} - ${this.page === 'leavitt-user-feedback' - ? html` ` - : nothing} - ${this.page === 'leavitt-error-page' - ? html` ` - : nothing} - ${this.page === 'leavitt-person-company-select' - ? html` ` - : nothing} - ${this.page === 'leavitt-person-group-select' - ? html` ` - : nothing} - ${this.page === 'titanium-drawer' ? html` ` : nothing} - ${this.page === 'profile-picture' ? html` ` : nothing} - ${this.page === 'profile-picture-menu' - ? html` ` - : nothing} - ${this.page === 'titanium-input-validator' - ? html` ` - : nothing} - ${this.page === 'titanium-data-table' - ? html` ` - : nothing} - ${this.page === 'titanium-data-table-core' - ? html` ` - : nothing} - ${this.page === 'titanium-promise-tracking' - ? html` - - ` - : nothing} - ${this.page === 'titanium-data-table-header' - ? html` ` - : nothing} - ${this.page === 'titanium-data-table-item' - ? html` ` - : nothing} - ${this.page === 'titanium-access-denied-page' - ? html` ` - : nothing} - ${this.page === 'titanium-address-input' - ? html` ` - : nothing} - ${this.page === 'titanium-error-page' - ? html` ` - : nothing} - ${this.page === 'titanium-header' ? html` ` : nothing} - ${this.page === 'titanium-icon' ? html` ` : nothing} - ${this.page === 'titanium-icon-picker' - ? html` ` - : nothing} - ${this.page === 'titanium-page-control' - ? html` ` - : nothing} - ${this.page === 'titanium-date-input' - ? html` ` - : nothing} - ${this.page === 'titanium-search-input' - ? html` ` - : nothing} - ${this.page === 'titanium-toolbar' - ? html` ` - : nothing} - ${this.page === 'titanium-full-page-loading-indicator' - ? html` - - ` - : nothing} - ${this.page === 'titanium-loading-indicator' - ? html` ` - : nothing} - ${this.page === 'titanium-chip-multi-select' - ? html` ` - : nothing} - ${this.page === 'titanium-styles' ? html` ` : nothing} - ${this.page === 'titanium-snackbar' - ? html` ` - : nothing} - ${this.page === 'titanium-smart-attachment-input' - ? html` - - ` - : nothing} - ${this.page === 'titanium-card' ? html` ` : nothing} - ${this.page === 'titanium-chip' ? html` ` : nothing} - ${this.page === 'titanium-youtube-input' - ? html` ` - : nothing} - ${this.page === 'titanium-show-hide' - ? html` ` - : nothing} - ${this.page === 'titanium-duration-input' - ? html` ` - : nothing} - ${this.page === 'titanium-confirm-dialog' - ? html` ` - : nothing} - ${this.page === 'titanium-confirmation-dialog' - ? html` ` - : nothing} - ${this.page === 'titanium-profile-picture-stack' - ? html` - - ` - : nothing} - + ${this.page === 'available-cdn-icons' ? html`` : nothing} + ${this.page === 'titanium-date-range-selector' ? html`` : nothing} + ${this.page === 'leavitt-person-select' ? html`` : nothing} + ${this.page === 'leavitt-company-select' ? html`` : nothing} + ${this.page === 'leavitt-email-history-viewer' ? html`` : nothing} + ${this.page === 'leavitt-file-explorer' ? html`` : nothing} + ${this.page === 'leavitt-error-page' ? html`` : nothing} + ${this.page === 'leavitt-person-company-select' ? html`` : nothing} + ${this.page === 'leavitt-person-group-select' ? html`` : nothing} + ${this.page === 'titanium-drawer' ? html`` : nothing} + ${this.page === 'profile-picture' ? html`` : nothing} + ${this.page === 'profile-picture-menu' ? html`` : nothing} + ${this.page === 'titanium-input-validator' ? html`` : nothing} + ${this.page === 'titanium-data-table-core' ? html`` : nothing} + ${this.page === 'titanium-promise-tracking' ? html`` : nothing} + ${this.page === 'titanium-address-input' ? html`` : nothing} + ${this.page === 'titanium-icon-picker' ? html`` : nothing} + ${this.page === 'titanium-page-control' ? html`` : nothing} + ${this.page === 'titanium-date-input' ? html`` : nothing} + ${this.page === 'titanium-search-input' ? html`` : nothing} + ${this.page === 'titanium-toolbar' ? html`` : nothing} + ${this.page === 'titanium-chip-multi-select' ? html`` : nothing} + ${this.page === 'titanium-styles' ? html`` : nothing} + ${this.page === 'titanium-snackbar' ? html`` : nothing} + ${this.page === 'titanium-smart-attachment-input' ? html`` : nothing} + ${this.page === 'titanium-chip' ? html`` : nothing} + ${this.page === 'titanium-youtube-input' ? html`` : nothing} + ${this.page === 'titanium-show-hide' ? html`` : nothing} + ${this.page === 'titanium-duration-input' ? html`` : nothing} + ${this.page === 'titanium-confirmation-dialog' ? html`` : nothing} + ${this.page === 'titanium-profile-picture-stack' ? html`` : nothing} - `; } } diff --git a/packages/leavittbook/src/services/auth-identity-controller.ts b/packages/leavittbook/src/services/auth-identity-controller.ts new file mode 100644 index 000000000..213e54c0f --- /dev/null +++ b/packages/leavittbook/src/services/auth-identity-controller.ts @@ -0,0 +1,29 @@ +import { ReactiveController, ReactiveControllerHost } from 'lit'; +import { AuthZeroLgIdenitity } from '@leavittsoftware/web/leavitt/user-manager/auth-zero-lg-identity'; + +import UserManager from './user-manager-service'; + +export class AuthIdentityController implements ReactiveController { + #host: ReactiveControllerHost; + + identity: AuthZeroLgIdenitity | null = null; + + #onIdentityUpdated = (identity: AuthZeroLgIdenitity | null) => { + this.identity = identity; + this.#host.requestUpdate(); + }; + + constructor(host: ReactiveControllerHost) { + this.#host = host; + host.addController(this); + } + + hostConnected() { + this.identity = UserManager.identity; + UserManager.onIdentityUpdated(this.#onIdentityUpdated); + } + + hostDisconnected() { + UserManager.removeOnIdentityUpdated(this.#onIdentityUpdated); + } +} diff --git a/packages/leavittbook/src/shared/npm-stats.ts b/packages/leavittbook/src/shared/npm-stats.ts index f2e8224d3..d3ea3683a 100644 --- a/packages/leavittbook/src/shared/npm-stats.ts +++ b/packages/leavittbook/src/shared/npm-stats.ts @@ -6,10 +6,10 @@ import { CountUp } from 'countup.js'; export default class NpmStats extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'hide-downloads' }) accessor hideDownloads: boolean = false; - @query('span.major') protected accessor major: HTMLDivElement; - @query('span.minor') protected accessor minor: HTMLDivElement; - @query('span.rev') protected accessor rev: HTMLDivElement; - @query('span.downloads') protected accessor downloads: HTMLDivElement; + @query('span.major') protected accessor major!: HTMLDivElement; + @query('span.minor') protected accessor minor!: HTMLDivElement; + @query('span.rev') protected accessor rev!: HTMLDivElement; + @query('span.downloads') protected accessor downloads!: HTMLDivElement; #package = '@leavittsoftware%2Fweb'; diff --git a/packages/leavittbook/src/shared/story-header.ts b/packages/leavittbook/src/shared/story-header.ts index ecc626458..7fa04e795 100644 --- a/packages/leavittbook/src/shared/story-header.ts +++ b/packages/leavittbook/src/shared/story-header.ts @@ -2,8 +2,11 @@ import { css, html, LitElement, nothing, PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { p } from '@leavittsoftware/web/titanium/styles/styles'; -import '@material/web/chips/suggestion-chip'; +import '@leavittsoftware/web/titanium/chip/chip'; +import '@material/web/button/filled-tonal-button'; import { heroStyles } from '../styles/hero-styles'; +import UserManager from '../services/user-manager-service'; +import { AuthIdentityController } from '../services/auth-identity-controller'; type CustomElementDeclaration = { name?: string; @@ -37,18 +40,20 @@ const readCustomElementsJson = async (path: string) => { @customElement('story-header') export default class StoryHeader extends LitElement { - @property({ type: String }) accessor name: string; - @property({ type: String }) accessor className: string; - @property({ type: String }) accessor deprecatedReason: string; + @property({ type: String }) accessor name: string = ''; + @property({ type: String }) accessor className: string = ''; + @property({ type: String }) accessor deprecatedReason: string = ''; + @property({ type: Boolean, attribute: 'requires-auth' }) accessor requiresAuth: boolean = false; @state() private accessor customElementDeclaration: CustomElementDeclaration | null = null; @state() private accessor customElementsJSON: { modules: [{ declarations: Array }] } | null = null; + #auth = new AuthIdentityController(this); + async updated(changedProps: PropertyValues) { if (changedProps.has('className') && this.className) { this.customElementsJSON = await getCustomElementsJSON(); this.customElementDeclaration = this.customElementsJSON?.modules.flatMap((o) => o.declarations).find((o) => o.name === this.className) ?? null; - console.log(this.customElementDeclaration); } } @@ -92,7 +97,10 @@ export default class StoryHeader extends LitElement { ${this.customElementDeclaration?.tagName ? html`

${'<'}${this.customElementDeclaration?.tagName}${'>'}

` : ''} - ${this.deprecatedReason ? html`` : nothing} + ${this.requiresAuth && !this.#auth.identity + ? html` void UserManager.authenticate()}>Authentication is required for this demo` + : nothing} + ${this.deprecatedReason ? html`` : nothing} `; } diff --git a/packages/leavittbook/src/styles/hero-styles.ts b/packages/leavittbook/src/styles/hero-styles.ts index 77ae6e4c9..47ddbe32f 100644 --- a/packages/leavittbook/src/styles/hero-styles.ts +++ b/packages/leavittbook/src/styles/hero-styles.ts @@ -26,9 +26,9 @@ export const heroStyles = css` padding: 0; } - [heading2], + [heading3], h3 { - font-family: var(--titanium-styles-h2-font-family, Metropolis, Roboto, Noto, sans-serif); + font-family: var(--titanium-styles-h3-font-family, Metropolis, Roboto, Noto, sans-serif); -webkit-font-smoothing: antialiased; font-size: 20px; line-height: 24px; @@ -38,4 +38,17 @@ export const heroStyles = css` margin: 0; padding: 0; } + + [heading4], + h4 { + font-family: var(--titanium-styles-h4-font-family, Metropolis, Roboto, Noto, sans-serif); + -webkit-font-smoothing: antialiased; + font-size: 16px; + line-height: 20px; + font-weight: 700; + letter-spacing: 0.2px; + + margin: 0; + padding: 0; + } `; diff --git a/packages/leavittbook/src/styles/my-app-styles.ts b/packages/leavittbook/src/styles/my-app-styles.ts index bb2e81fbd..72daeaaee 100644 --- a/packages/leavittbook/src/styles/my-app-styles.ts +++ b/packages/leavittbook/src/styles/my-app-styles.ts @@ -5,13 +5,26 @@ export const myAppStyles = css` display: grid; grid: 'toolbar toolbar' 64px - 'menu content' auto / 300px 1fr; + 'menu content' auto / 310px 1fr; transition: 250ms; --mdc-icon-font: 'Material Symbols Outlined'; } + :host([no-main-menu]) { + grid-template-columns: 0 1fr !important; + + titanium-toolbar { + grid-template-columns: auto auto 1fr auto !important; + padding: 0 24px 0 24px !important; + } + } + + :host([no-main-menu]) main-content { + margin-left: 16px; + } + :host([main-menu-position='drawer']) { grid: 'toolbar toolbar' 64px @@ -34,6 +47,10 @@ export const myAppStyles = css` } } + :host([no-main-menu][main-menu-position='drawer']) main-content { + margin-left: 0; + } + /* safari does not like nested selectors on host */ :host([main-menu-position='slim']) titanium-drawer[main-menu] { --titanium-drawer-width: 80px; @@ -79,10 +96,10 @@ export const myAppStyles = css` :host([main-menu-position='slim']) { grid: 'toolbar toolbar' 64px - 'menu content' auto / 80px 1fr; + 'menu content' auto / 85px 1fr; titanium-toolbar { - grid: 'main-menu-button logo search-input page-actions' / 80px auto 1fr auto; + grid: 'main-menu-button logo search-input page-actions' / 85px auto 1fr auto; } } @@ -170,12 +187,20 @@ export const myAppStyles = css` } h4[sub] { - display: flex; + display: grid; + overflow: hidden; + grid-template-columns: auto 1fr; align-items: center; gap: 4px; - padding: 0 12px 2px 35px; + padding: 0 12px 2px 24px; opacity: 0.8; --md-icon-size: 16px; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } @@ -183,7 +208,7 @@ export const myAppStyles = css` grid-area: toolbar; background-color: var(--app-background-color); display: grid; - grid: 'main-menu-button logo search-input page-actions' / auto 240px 1fr auto; + grid: 'main-menu-button logo search-input page-actions' / auto 250px 1fr auto; align-items: center; height: 64px; z-index: 6; diff --git a/packages/leavittbook/src/styles/story-styles.ts b/packages/leavittbook/src/styles/story-styles.ts index d20e8bc10..5cb0d8e9c 100644 --- a/packages/leavittbook/src/styles/story-styles.ts +++ b/packages/leavittbook/src/styles/story-styles.ts @@ -11,10 +11,6 @@ const StoryStyles = [ margin-bottom: 48px; } - titanium-card { - margin-bottom: 36px; - } - h1 { margin-bottom: 0; } diff --git a/packages/web/.ncurc.js b/packages/web/.ncurc.js index 333acf774..8f6fb8a94 100644 --- a/packages/web/.ncurc.js +++ b/packages/web/.ncurc.js @@ -1,4 +1,4 @@ -const rejectAll = ['@googlemaps/js-api-loader']; +const rejectAll = []; const rejectMajor = []; module.exports = { diff --git a/packages/web/CLAUDE.md b/packages/web/CLAUDE.md new file mode 100644 index 000000000..d93dc84e9 --- /dev/null +++ b/packages/web/CLAUDE.md @@ -0,0 +1,1320 @@ +# @leavittsoftware/web + +Component and utility reference for agents consuming this package. Read this before diving into source. + +## Package overview + +`@leavittsoftware/web` is a collection of Lit 3 web components built on [Material Web](https://github.com/material-components/material-web). + +- **`titanium-*`** — general-purpose UI (drawers, tables, inputs, snackbars, loading indicators) +- **`leavitt-*`** — Leavitt Group domain components (selects, file explorer, app shell, email viewer); many require `ApiService` and authentication + +**Browser support:** Chrome, Safari, Firefox, Edge + +**Install:** + +```bash +npm i @leavittsoftware/web +``` + +## Upgrade changelog (for agents) + +When bumping `@leavittsoftware/web` in a downstream project, read every entry **after** the version currently installed. Each entry lists grep targets, removals, and replacements — not full release history (that lives in-repo at `CHANGELOG.md`). + +### Unreleased + +**Upgrade if coming from:** `< unreleased` (published latest is `9.9.0`) + +**Search downstream for:** + +- `LoadWhile` — mixin import or `extend LoadWhile(` on **page** components only (not `dataTable.loadWhile`) +- `titanium-data-table` tag or import (legacy stack — not `data-table-core`) +- `titanium-confirm-dialog` (not `confirmation-dialog`) +- `titanium-error-page`, `titanium-access-denied-page`, `titanium-full-page-loading-indicator` +- `titanium-card`, `titanium-header` +- `leavitt-user-feedback` (shell component) +- `leavitt/email-history-viewer/email-history-viewer` (unfilled legacy viewer) +- `outlined-duration-input`, `outlined-input-validator`, unqualified `search-input` / `youtube-input` / `duration-input` import paths +- `filled` attribute on date-input, date-range-selector, chip, chip-multi-select, page-control, show-hide, smart-attachment-input, single-select-base subclasses, manual-address-dialog +- `always-show-content` on `titanium-drawer` +- `toolbarSearchTerm`, `toolbar-search-term` on `leavitt-email-history-viewer-filled` + +**Removed** — delete imports/usages: + +| Removed | Replacement | +| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `@leavittsoftware/web/titanium/helpers/load-while` | `@promiseTracking` on page components — see Loading — `promiseTracking` | +| `titanium-confirm-dialog` | `titanium-confirmation-dialog` | +| `titanium-error-page` | `leavitt-error-page` | +| Legacy `titanium-data-table`, `-item`, `-header` | `titanium-data-table-core` + action bar + page control | +| `leavitt-user-feedback` | `provide-feedback-dialog` / `report-a-problem-dialog` directly | +| `leavitt/email-history-viewer/email-history-viewer` | `leavitt-email-history-viewer-filled` only | +| `titanium-card`, `titanium-header`, `titanium-access-denied-page`, `titanium-full-page-loading-indicator` | App-specific UI (no direct replacement) | +| Outlined / unqualified input variants | Filled paths only (e.g. `filled-search-input`, `filled-duration-input`) | +| `always-show-content` on `titanium-drawer` | Set `mode="inline"` / `mode="flyover"` and call `open()` for inline sidebars | +| `toolbarSearchTerm` / `toolbar-search-term` on `leavitt-email-history-viewer-filled` | `.siteSearchTextFieldContext` + `TitaniumSiteSearchTextFieldController` — see Site search | + +**Renamed / API changes:** + +- Page-level `LoadWhile` mixin / `this.loadWhile()` on the page host → `@promiseTracking` + `trackLoadingPromise` (see Loading — `promiseTracking`) +- `dataTable.loadWhile(promise)` — **still supported** via deprecated alias on `titanium-data-table-core`; prefer `trackLoadingPromise` in new code +- `itemMetaData[].sortExpression` → `getSortExpression: () => string` (since 9.0; still required if not yet migrated) + +**Behavior / styling:** + +- `filled` attribute removed from dual-style components — filled Material styling is always on +- `google-address-input` uses `@googlemaps/js-api-loader` v2 internally; consumers still only pass `googleMapsApiKey` — no bundler `process` shim required. Enable **Places API (New)** (`places.googleapis.com`) plus **Maps JavaScript API** on the key's GCP project (legacy Places API is not enough). +- `leavitt-error-page` no longer uses tsParticles; no consumer particle config to migrate + +### 9.4.0 + +**Upgrade if coming from:** `< 9.4.0` + +**Adopt before unreleased / 10.x:** + +- `@promiseTracking` decorator added — preferred for page-level loading flags +- If page components still `extend LoadWhile(LitElement)`, migrate to `@promiseTracking` + `trackLoadingPromise` before upgrading past 9.9.x +- `dataTable.loadWhile(promise)` call sites do **not** need changing (compat alias on `titanium-data-table-core`) + +### 9.0.0 + +**Upgrade if coming from:** `< 9.0.0` + +**Search downstream for:** + +- `UserManager`, `GetUserManagerInstance`, `AuthenticatedTokenProvider`, `UserManagerUpdatedEvent` +- `sortExpression:` in `TitaniumDataTableCoreMetaData` column defs + +**Renamed / API changes:** + +- Auth helpers above → `AuthZeroLgUserManager` (see Services — `AuthZeroLgUserManager`) +- `itemMetaData[].sortExpression: 'Name'` → `getSortExpression: () => 'Name'` + +## Import and registration + +There is no barrel `index.ts` and no `exports` map. Always use deep file paths: + +```ts +// Register element (side-effect — required before using the tag in HTML) +import '@leavittsoftware/web/titanium/drawer/drawer.js'; + +// Import class, types, or styles +import { TitaniumDrawer } from '@leavittsoftware/web/titanium/drawer/drawer.js'; +import { h2, p } from '@leavittsoftware/web/titanium/styles/styles.js'; +``` + +- Published builds use `.js` extensions in import paths +- Material Web elements (`md-filled-button`, `md-dialog`, etc.) must be imported separately by the consumer +- Paths mirror source: `titanium//` or `leavitt//` + +## Material Web foundation + +Most `titanium-*` and `leavitt-*` elements are **built on** [Material Web](https://github.com/material-components/material-web) (`@material/web/*`) — either by **extending** an `Md*` class or by **composing** `md-*` tags inside a `LitElement` wrapper. When debugging API, validation, slots, or styling, check the matching Material Web component docs in addition to this file. + +### Extend vs compose + +| Pattern | What it means | Where to look for extra API | +| ------- | ------------- | --------------------------- | +| **Extends** | `class TitaniumFoo extends MdFilledTextField` (or `MdFilledField`, etc.) | Material Web docs for that base class — properties, attributes, methods, slots, and CSS parts apply on the **titanium tag** | +| **Composes** | `class TitaniumFoo extends LitElement` and renders `` (or `md-dialog`, `md-menu`, …) in `render()` | This file + Material Web docs for the **inner** `md-*` tag; the titanium tag exposes its own curated API | + +Titanium **mixins and decorators** (`ThemePreference`, `promiseTracking`, …) apply to the **host class** they decorate. They do not replace Material Web behavior — an input that extends `MdFilledTextField` still has all text-field properties; one that composes `md-filled-text-field` exposes titanium properties on the host and Material Web behavior on the child. + +### Published components by pattern + +| Tag / family | Pattern | Material Web base or key children | +| ------------ | ------- | --------------------------------- | +| `titanium-filled-duration-input` | extends | `MdFilledTextField` | +| `titanium-filled-youtube-input` | extends | `MdFilledTextField` | +| `titanium-filled-input-validator` | extends | `MdFilledField` | +| `titanium-filled-search-input` | composes | `md-filled-text-field` | +| `titanium-date-input` | composes | `md-filled-field` | +| `titanium-date-range-selector` | composes | `md-filled-field`, `md-menu`, `md-list` | +| `titanium-single-select-base` and all `leavitt-*-select`, `titanium-icon-picker`, `google-address-input` | composes | `md-filled-text-field`, `md-menu`, `md-menu-item` | +| `titanium-chip-multi-select` | composes | `titanium-filled-input-validator` (extends `MdFilledField`); slots intended for `md-filled-tonal-button`, `md-input-chip` | +| `titanium-confirmation-dialog` and most modal/dialog components | composes | `md-dialog` (+ `md-filled-button` / `md-text-button` actions) | +| `titanium-data-table-core` | composes | `md-checkbox`, `md-icon-button`, `md-menu` | +| `titanium-page-control` | composes | `md-filled-select` | +| `titanium-chip` | composes | `md-ripple`, `md-focus-ring` (custom chip — not `md-chip`) | +| Snackbars, toolbars, many app-shell pieces | composes | assorted `md-icon`, `md-icon-button`, `md-filled-button`, etc. | + +When a component **extends** `MdFilledTextField` or `MdFilledField`, consumers can use standard Material field properties on the titanium tag: `label`, `error`, `error-text`, `supporting-text`, `required`, `disabled`, `prefix-text`, `suffix-text`, `checkValidity()`, `reportValidity()`, `setCustomValidity()`, leading/trailing icon slots, and documented CSS parts. + +### Styling + +Material Web theming uses **CSS custom properties** on `:host` or on a child `md-*` selector: + +```css +:host { + --md-filled-text-field-container-shape: 24px; + --md-filled-text-field-container-color: var(--md-sys-color-surface-container-high); + --md-sys-color-primary: /* app theme */; +} +``` + +- `--md-sys-color-*` — Material 3 color roles (surface, on-surface, primary, outline-variant, …) +- `--md-filled-text-field-*`, `--md-filled-field-*`, `--md-dialog-*`, `--md-icon-button-*`, … — per-component tokens + +Set tokens on the titanium host when the inner `md-*` element inherits from `:host`, or target the child directly (e.g. `md-filled-text-field { … }` in the component's `static styles`). See `titanium-filled-search-input` and `titanium-drawer` for examples. + +### Internal utilities from Material Web + +Some titanium components import Material Web **internals** (not part of the public consumer API): + +- `redispatchEvent` from `@material/web/internal/events/redispatch-event` — re-bubbles native/MW events from composed children +- `stringConverter` and field internals — used by `titanium-date-input` for form-associated behavior + +Do not import `@material/web/internal/*` from consuming applications unless Material Web documents those paths as stable. + +## Tag naming caveats + +Several elements omit the `titanium-` / `leavitt-` prefix: + +`profile-picture`, `profile-picture-menu`, `report-a-problem-dialog`, `provide-feedback-dialog`, `google-address-input`, `manual-address-dialog`, `crop-and-save-image-dialog`, `image-preview-dialog`, `file-list-item`, `folder-list-item`, `data-table-core-settings-sort-item` + +## Cross-cutting patterns + +### Loading — `promiseTracking` + +```ts +import { promiseTracking } from '@leavittsoftware/web/titanium/helpers/promise-tracking'; + +@promiseTracking('trackSavingPromise') +@state() accessor isSaving = false; +declare trackSavingPromise: (promise: Promise) => Promise; + +async #save() { + const post = api.postAsync('...', dto); + this.trackSavingPromise(post); + await post; +} +``` + +Supports multiple independent flags per component (`isSaving` vs `isDeleting`). Re-entrant: flag stays true until all concurrent promises settle. + +### Ancestor loading — `PendingStateEvent` + +```ts +import { PendingStateEvent } from '@leavittsoftware/web/titanium/types/pending-state-event'; + +const promise = api.getAsync('...'); +this.dispatchEvent(new PendingStateEvent(promise)); +await promise; +``` + +Bubbles and is composed. Wire `.pendingStateElement=${this}` on `leavitt-app-main-content-container` or `titanium-circle-loading-indicator`. The circle indicator calls `stopPropagation()` so drawer events don't light sibling containers. + +Use **either** a local spinner **or** `PendingStateEvent` for the same promise — not both unless intentional (e.g. `isLoading` gates empty state while circle shows overlay). + +### Toasts — `ShowSnackbarEvent` + +```ts +import { ShowSnackbarEvent } from '@leavittsoftware/web/titanium/snackbar/show-snackbar-event'; + +this.dispatchEvent(new ShowSnackbarEvent('Saved successfully')); +this.dispatchEvent(new ShowSnackbarEvent(httpError)); // HttpError object +``` + +Place `` in the app shell; it listens for `show-snackbar` on `document` or a custom `.eventListenerTarget`. + +### URL filters — `FilterController` + +```ts +import { FilterController } from '@leavittsoftware/web/titanium/data-table/filter-controller'; + +filterController = new FilterController('my-page-route'); +// Register Filter instances; syncs with query string; getActiveFilterOdata() for OData $filter +``` + +The constructor `path` should match the page's route path. + +### Site search — `TitaniumSiteSearchTextFieldController` + +```ts +import { TitaniumSiteSearchTextFieldController } from '@leavittsoftware/web/titanium/site-search-text-field-controller/site-search-text-field-controller'; + +searchController = new TitaniumSiteSearchTextFieldController(this, siteSearchTextFieldContext, { + placeholder: 'Search...', + onSearch: () => this.#reload(), +}); +``` + +Page host must expose `isActive: boolean`. Read `this.searchController.searchTerm` when fetching. Toggle `this.searchController.disabled` when search requires a prerequisite (e.g. company selected). + +### OData HTTP — `ApiService` + +Required by `leavitt-*-select`, `leavitt-file-explorer`, email viewers. Pass a configured singleton: + +```ts +import ApiService from '@leavittsoftware/web/leavitt/api-service/api-service'; + +const result = await apiService.getAsync>('People/?$top=25&$count=true'); +const items = result.toList(); +const total = result.odataCount; +``` + +### Typography styles + +Always import shared text styles before custom font sizing: + +```ts +import { h2, h5, p } from '@leavittsoftware/web/titanium/styles/styles.js'; + +static styles = [h2, h5, p, css`...`]; +``` + +Exports: `h1`–`h5` (aliases `heading1`–`heading5`), `p`/`paragraph`, `a`/`link`, `dataRow`, `ellipsis`, `niceBadgeStyles` (from `titanium/styles/nice-badge`). + +### Status pills in data tables + +`titanium-data-table-core` injects `data-table-content-styles`. In column `render` functions: + +```html +Neutral +Active +Inactive +``` + +Only green / red / neutral-gray — no orange/warning tier. + +### Multi-word HTML attributes + +Use kebab-case for reflected attributes: `local-storage-key="my-key"`, not `localStorageKey="my-key"`. Use `.localStorageKey=${value}` only for dynamic property binding. + +--- + +## Data tables + +Use `titanium-data-table-core` + `titanium-data-table-action-bar` + `titanium-page-control` together for list pages. + +Define columns via `TitaniumDataTableCoreMetaData`: + +```ts +tableMetaData: TitaniumDataTableCoreMetaData = { + uniqueKey: (item) => String(item.Id), + itemLinkUrl: (item) => `/items/${item.Id}`, + itemMetaData: [{ key: 'name', friendlyName: 'Name', render: (item) => html`${item.Name}`, getSortExpression: () => 'Name' }], + reorderConfig: { sortPropertyKey: 'SortOrder', reorderItemDisplayKey: 'Name' }, +}; +``` + +- Sort/column prefs persist in `localStorage` under `local-storage-key` +- `friendlyName` values: sentence case +- Reorder: listen `@reorder-save-request`, call `e.detail.resolve()` on success or `e.detail.reject(error)` on failure — the reorder dialog shows errors via its own snackbar; don't snackbar in the save handler +- On refetch: don't clear rows (layout jank); use `trackLoadingPromise` / `disabled` on the table +- Dispatches `change-route` (composed) when row has `itemLinkUrl`; `items-reordered` when reorder dialog applies + +## Inheritance bases + +Titanium class inheritance (in addition to Material Web extend/compose — see **Material Web foundation**): + +| Base | Path | Used by | +| -------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- | +| `MdFilledTextField` | `@material/web/textfield/filled-text-field` | `titanium-filled-duration-input`, `titanium-filled-youtube-input` | +| `MdFilledField` | `@material/web/field/filled-field` | `titanium-filled-input-validator` | +| `TitaniumSingleSelectBase` | `titanium/single-select-base/single-select-base` | All `leavitt-*-select`, `titanium-icon-picker`, `google-address-input` | +| `google-address-input` | `titanium/address-input/google-address-input` | `titanium-address-input` | +| `ThemePreference` mixin | `leavitt/theme/theme-preference` | `leavitt-app-logo`, `leavitt-error-page`, `leavitt-service-worker-notifier`, `titanium-single-select-base` | + +--- + +# Components + +## App shell + +### `leavitt-app-main-content-container` + +**Purpose:** Main scrollable content area with loading overlay; adapts layout when main menu is in drawer mode. + +**Import:** `import '@leavittsoftware/web/leavitt/app/app-main-content-container.js'` + +**Source:** `leavitt/app/app-main-content-container.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ---------------------- | ----------------- | ----------------------------------------------------------------------- | +| Attribute | `main-menu-position` | `string` | From `mainMenuPositionContext`; `'drawer'` changes height/border-radius | +| Property | `.pendingStateElement` | `Element \| null` | Host that dispatches `PendingStateEvent` for the circle loader | + +**Methods:** — + +**Events:** Listens for `pending-state` on `pendingStateElement` + +**Slots:** default + +**CSS parts:** `loading-indicator`, `scroll-container` + +**Usage notes / gotchas:** + +- Height is `100dvh`-based; drawer mode removes right margin and border radius +- When kicking off async work in `updated()`, `await this.appMainContentContainer?.updateComplete` first so the loader can appear + +**Pairs with:** `titanium-circle-loading-indicator`, `mainMenuPositionContext` + +--- + +### `leavitt-app-navigation-header` + +**Purpose:** Sticky app header with up to 5-level breadcrumb trail. + +**Import:** `import '@leavittsoftware/web/leavitt/app/app-navigation-header.js'` + +**Source:** `leavitt/app/app-navigation-header.ts` + +| Kind | Name | Type / values | Notes | +| --------- | --------------------------- | ----------------- | ---------------------- | +| Attribute | `sticky-top` | `boolean` | Reflected | +| Property | `scrollable-parent` | `Element \| null` | Auto-detected if unset | +| Property | `level1Text` … `level5Text` | `string \| null` | Breadcrumb labels | +| Property | `level1Href` … `level5Href` | `string \| null` | Breadcrumb links | + +**Methods:** — + +**Events:** — + +**Slots:** `trailing`, `footer` + +**CSS parts:** `main`, `trailing`, `footer` + +**Usage notes / gotchas:** + +- Border appears when scrolled + `sticky-top` +- Top-level nav labels should match page header text (level1 = top nav, level3 = sub-nav) + +--- + +### `leavitt-app-navigation-footer` + +**Purpose:** Sticky bottom bar with leading/default/trailing action areas. + +**Import:** `import '@leavittsoftware/web/leavitt/app/app-navigation-footer.js'` + +**Source:** `leavitt/app/app-navigation-footer.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ------------------- | ----------------- | ----------------- | +| Attribute | `max-width` | `string` | Default `'640px'` | +| Property | `scrollable-parent` | `Element \| null` | | + +**Methods:** `isOverflown(element: Element): boolean` + +**Events:** — + +**Slots:** `leading`, default, `trailing` + +**CSS parts:** `main`, `leading`, `trailing` + +**Usage notes / gotchas:** Shows top border when scroll parent overflows + +--- + +### `leavitt-app-logo` + +**Purpose:** App name + Leavitt Group mark with hover animation; theme-aware CDN mark. + +**Import:** `import '@leavittsoftware/web/leavitt/app/app-logo.js'` + +**Source:** `leavitt/app/app-logo.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ---------- | ---------------- | -------------------------------- | +| Property | `href` | `string` | Default `'/'` | +| Property | `title` | `string` | | +| Attribute | `app-name` | `string \| null` | Title case; acronyms keep casing | + +**Methods / events / slots / parts:** — + +**Usage notes / gotchas:** Extends `ThemePreference` mixin; mark URL switches for dark theme + +--- + +### `leavitt-app-width-limiter` + +**Purpose:** Centers content with configurable max width. + +**Import:** `import '@leavittsoftware/web/leavitt/app/app-width-limiter.js'` + +**Source:** `leavitt/app/app-width-limiter.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ----------- | ------------- | ----------------- | +| Attribute | `max-width` | `string` | Default `'640px'` | + +**Slots:** default + +**Usage notes / gotchas:** Parent must be `display: grid` or `display: flex` for `justify-self: center` to work (Firefox) + +--- + +## Layout and navigation + +### `titanium-drawer` + +**Purpose:** Fly-out / inline drawer based on native ``. + +**Import:** `import '@leavittsoftware/web/titanium/drawer/drawer.js'` + +**Source:** `titanium/drawer/drawer.ts` + +| Kind | Name | Type / values | Notes | +| --------- | --------------------------------- | --------------------- | ----------------------------------------------- | +| Attribute | `mode` | `inline` \| `flyover` | | +| Attribute | `open` | `boolean` | Read-only; reflected from `isOpen` | +| Attribute | `direction` | `ltr` \| `rtl` | Animation direction | +| Attribute | `fixed` | `boolean` | Content position when closed (inline mode only) | +| Attribute | `keep-open-when-going-to-flyover` | `boolean` | Preserve open state on mode switch | + +**Methods:** `open()`, `close()`, `toggle()`, `closeQuick()` + +**Events:** `open-change`; redispatches native `close`, `toggle` from dialog + +**Slots:** default, `header`, `footer` + +**CSS parts:** `dialog`, `header`, `main`, `footer` + +**CSS custom properties:** `--md-sys-color-outline-variant`, `--md-sys-color-on-background` + +**Usage notes / gotchas:** + +- Swipe-left closes; backdrop click closes; `popstate` closes dialog +- Flyover sets `html { overflow: hidden }` +- Inline sidebar: set `mode="inline"` and call `open()`; do not use removed `always-show-content` +- Switching `mode` from inline→flyover: use `keep-open-when-going-to-flyover` to stay open + +--- + +### `titanium-toolbar` + +**Purpose:** Fixed top Material toolbar with scroll-based elevation. + +**Import:** `import '@leavittsoftware/web/titanium/toolbar/toolbar.js'` + +**Source:** `titanium/toolbar/toolbar.ts` + +| Kind | Name | Type / values | Notes | +| --------- | -------- | ------------- | --------------------------- | +| Attribute | `shadow` | `boolean` | Auto-set on document scroll | + +**Slots:** default (style slotted `[main-title]`) + +**Usage notes / gotchas:** Listens on `document` scroll, not a local container + +--- + +### `titanium-show-hide` + +**Purpose:** Collapsible overflow content with optional fade and custom toggle button. + +**Import:** `import '@leavittsoftware/web/titanium/show-hide/show-hide.js'` + +**Source:** `titanium/show-hide/show-hide.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ----------------- | ------------- | -------------------------------- | +| Attribute | `collapse-height` | `number` | Default `120` | +| Attribute | `collapsed` | `boolean` | | +| Attribute | `disable-fade` | `boolean` | | +| Property | `hiddenItemCount` | `number` | Read-only count of clipped items | + +**Events:** `collapsed-changed`, `hidden-item-count-changed` + +**Slots:** default (direct children), `button` + +**CSS parts:** `items-container`, `button` + +**Usage notes / gotchas:** ResizeObserver counts clipped children; default button hidden when nothing is hidden + +--- + +### `titanium-collapsible-container` + +**Purpose:** Expand/collapse panel with header slot. + +**Import:** `import '@leavittsoftware/web/titanium/collapsible-container/collapsible-container.js'` + +**Source:** `titanium/collapsible-container/collapsible-container.ts` + +| Kind | Name | Type / values | Notes | +| --------- | -------------------- | ------------- | --------- | +| Attribute | `opened`, `disabled` | `boolean` | Reflected | + +**Slots:** `header`, `content` + +**CSS parts:** `button`, `main` + +**Usage notes / gotchas:** Toggles `opened` on header button click + +--- + +## Data tables + +### `titanium-data-table-core` + +**Purpose:** Metadata-driven table with sort, column picker, CSV export, selection, and optional reorder. + +**Import:** `import '@leavittsoftware/web/titanium/data-table/data-table-core.js'` + +**Source:** `titanium/data-table/data-table-core.ts` + +| Kind | Name | Type / values | Notes | +| --------- | --------------------------- | ------------------------------------------ | ---------------------------------------------- | +| Property | `items` | `T[]` | Current rows | +| Property | `tableMetaData` | `TitaniumDataTableCoreMetaData \| null` | Column config | +| Property | `selected` | `T[]` | Selected rows | +| Attribute | `selection-mode` | `single` \| `multi` \| `none` | Default `none` | +| Attribute | `local-storage-key` | `string` | Default `'dtc-pref'`; **kebab-case attribute** | +| Attribute | `sticky-header`, `disabled` | `boolean` | | +| Property | `sort` | getter/setter | Persists to `{key}-user-sort` in localStorage | +| Property | `userSettings` | getter/setter | Column visibility prefs | +| Property | `supplementalItemStyles` | `CSSResult \| CSSResultGroup \| null` | Per-row styles | +| State | `isLoading` | `boolean` | Via `promiseTracking` | + +**Methods:** `selectAll()`, `deselectAll()`, `resetSort()`, `trackLoadingPromise(promise)`, `loadWhile(promise)` (deprecated alias for `trackLoadingPromise`) + +**Events:** + +- `selected-changed` (composed) +- `sort-changed` +- `items-reordered` — `CustomEvent` +- `change-route` — `CustomEvent<{ path: string }>` (composed) +- `reorder-save-request` — delegate with `resolve()` / `reject(error)` + +**Slots:** `settings-menu-items` + +**CSS parts:** `table` + +**Exported types:** `TitaniumDataTableCoreMetaData`, `TitaniumDataTableCoreItemMetaData`, `TitaniumDataTableCoreSortItem`, `generateDefaultSortFromMetaData()` + +**Usage notes / gotchas:** + +- Clears selection when `items` reference changes +- Don't clear rows on refetch (scrollbar jank); disable controls via `disabled` / `isLoading` +- Status columns: use `` / `red` (see cross-cutting patterns) +- `friendlyName` on column metadata: sentence case + +--- + +### `titanium-data-table-action-bar` + +**Purpose:** Filter/add button bar that swaps to bulk selection actions when rows are selected. + +**Import:** `import '@leavittsoftware/web/titanium/data-table/data-table-action-bar.js'` + +**Source:** `titanium/data-table/data-table-action-bar.ts` + +| Kind | Name | Type / values | Notes | +| -------- | ---------- | -------------- | ----- | +| Property | `selected` | `Partial[]` | | + +**Slots:** `add-button`, `filters`, `selected-actions` + +**CSS parts:** `main`, `add-button-container`, `filters-container`, `selected-action-veil`, `selected-action-title`, `action-container` + +**Pairs with:** `titanium-data-table-core` + +--- + +### `titanium-page-control` + +**Purpose:** Page size selector + prev/next paging control. + +**Import:** `import '@leavittsoftware/web/titanium/data-table/page-control.js'` + +**Source:** `titanium/data-table/page-control.ts` + +| Kind | Name | Type / values | Notes | +| --------- | ------------------- | ------------- | -------------------------------------------- | +| Property | `pageSizes` | `number[]` | Default `[10,15,20,50]` | +| Attribute | `default-page-size` | `number` | Default `10` | +| Property | `page` | `number` | Zero-based | +| Property | `count` | `number` | Total items | +| Attribute | `local-storage-key` | `string` | **Required** for `take` persistence | +| Property | `label` | `string` | Default `'Items per page'` | +| Property | `disabled` | `boolean` | | +| Property | `take` | getter/setter | Persists to localStorage; resets `page` to 0 | + +**Events:** `action` (composed) — fired on page or take change + +**Usage notes / gotchas:** + +- Unknown `take` values are added to `pageSizes` +- Next disabled when `(page+1)*take >= count` +- Use `local-storage-key` attribute (kebab-case), not `localStorageKey` + +--- + +## Form inputs + +### `titanium-date-input` + +**Purpose:** Cross-browser date / datetime-local input (form-associated). + +**Material Web:** Composes `md-filled-field`; uses MW field tokens and `redispatchEvent` for form events. + +**Import:** `import '@leavittsoftware/web/titanium/date-input/date-input.js'` + +| Kind | Name | Type / values | Notes | +| --------- | ------------------------------------------------------ | -------------------------- | ----- | +| Property | `value`, `label`, `placeholder`, `supporting-text` | `string` | | +| Attribute | `type` | `date` \| `datetime-local` | | +| Attribute | `min`, `max`, `maxLength` | `string` | | +| Property | `required`, `disabled`, `error` | `boolean` | | +| Property | `prefix-text`, `suffix-text` | `string` | | +| Attribute | `has-leading-icon`, `has-trailing-icon`, `no-asterisk` | `boolean` | | + +**Methods:** `checkValidity()`, `reportValidity()`, `select()`, `setCustomValidity()`, `reset()` + +**Events:** Redispatches `change`, `blur`, `select`, `invalid` + +**Slots:** `leading-icon`, `trailing-icon` (default trailing: calendar picker) + +**Usage notes / gotchas:** `formAssociated`; after user input, attribute `value` no longer syncs until `reset()`; Safari/iOS/Firefox-specific styling + +--- + +### `titanium-date-range-selector` + +**Purpose:** Preset + custom date range picker with popover UI. + +**Material Web:** Composes `md-filled-field`, `md-menu`, `md-list`, `md-text-button`. + +**Import:** `import '@leavittsoftware/web/titanium/date-range-selector/date-range-selector.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------------------- | -------------------------------------- | ------------------------------------------------------------- | +| Property | `range` | `string` | Default `'custom'`; presets like `allTime`, `last7Days`, etc. | +| Property | `startDate`, `endDate` | `string` | ISO date strings | +| Property | `label`, `supporting-text`, `type` | `string` | `type`: `date` \| `datetime-local` | +| Property | `customDateRanges` | `Map \| null` | Override presets | +| Property | `disabled`, `positioning` | | `positioning`: `popover` \| `fixed` | + +**Methods:** `reset()` — sets `range` to `'allTime'` + +**Events:** `change` on Set (note: `DateRangeChangedEvent` class exists but component fires `change`) + +**CSS parts:** `field` + +**Usage notes / gotchas:** Firefox lacks `popover` → falls back to `fixed`; datetime custom requires 16-char values to enable Set + +--- + +### `titanium-filled-duration-input` + +**Purpose:** Natural-language duration input (parses strings like "3 hours and 30 minutes"). + +**Material Web:** Extends `MdFilledTextField` — inherits label, validation, and field styling API. + +**Import:** `import '@leavittsoftware/web/titanium/duration-input/filled-duration-input.js'` + +**Source:** `titanium/duration-input/filled-duration-input.ts` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------- | ------------------------ | --------------- | +| Property | `duration` | `dayjs.Duration \| null` | Canonical value | +| Property | `label`, `placeholder` | `string` | | + +**Events:** `duration-change` — read `event.target.duration` + +**Usage notes / gotchas:** `value` is human-readable string; use `duration` property for logic + +--- + +### `titanium-filled-search-input` + +**Purpose:** Full-width filled search field with clear button. + +**Material Web:** Composes `md-filled-text-field`; style via `--md-filled-text-field-*` on `:host`. + +**Import:** `import '@leavittsoftware/web/titanium/search-input/filled-search-input.js'` + +**Source:** `titanium/search-input/filled-search-input.ts` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------------------------- | ------------- | ----- | +| Property | `value`, `placeholder` | `string` | | +| Property | `disabled`, `autocomplete`, `spellcheck` | | | + +**Events:** `input` (composed); redispatches `blur`, `focus`, `change`, `invalid` + +--- + +### `titanium-filled-youtube-input` + +**Purpose:** YouTube video key input; strips full URLs to 11-char key; shows thumbnail preview. + +**Material Web:** Extends `MdFilledTextField` — inherits label, validation, and trailing-icon slot. + +**Import:** `import '@leavittsoftware/web/titanium/youtube-input/filled-youtube-input.js'` + +**Source:** `titanium/youtube-input/filled-youtube-input.ts` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------- | ------------- | ------------------- | +| Property | `value`, `label` | `string` | | +| Property | `pattern` | `string` | Default `'^.{11}$'` | + +**Methods:** `reset()` + +**Usage notes / gotchas:** Shows thumbnail in trailing slot when key length is 11 + +--- + +### `titanium-address-input` + +**Purpose:** Google Places search with manual address entry fallback. + +**Import:** `import '@leavittsoftware/web/titanium/address-input/address-input.js'` + +| Kind | Name | Type / values | Notes | +| --------- | ------------------------------------ | ------------- | ------------------------ | +| Property | `googleMapsApiKey` | `string` | **Required** (inherited) | +| Attribute | `show-street2`, `show-county` | `boolean` | | +| Property | `allow-international` | `boolean` | | +| + | All `TitaniumSingleSelectBase` props | | | + +**Events:** `selected` (JSDoc says `location-changed` but base fires `selected`) + +**Usage notes / gotchas:** Requires Google Maps API key; opens `manual-address-dialog` for manual entry; US street validation when not international + +--- + +### `google-address-input` + +**Purpose:** Google Places autocomplete base (extended by `titanium-address-input`). + +**Import:** `import '@leavittsoftware/web/titanium/address-input/google-address-input.js'` + +| Kind | Name | Type / values | Notes | +| -------- | --------------------- | ------------- | ------------------------------ | +| Property | `googleMapsApiKey` | `string` | **Required** | +| Property | `pathToSelectedText` | `string` | Default `'primaryDisplayText'` | +| Property | `allow-international` | `boolean` | | + +**Events:** `selected`; dispatches `ShowSnackbarEvent` on API errors + +**Usage notes / gotchas:** Loads Google Maps via `@googlemaps/js-api-loader` v2, which reads `process.env.NODE_ENV` at module scope. The component ships a guarded global `process` shim (`google-maps-process-shim.ts`, imported first) so it works in browser environments that don't define `process`; consumers need no bundler define. **GCP:** the API key's project must have **Maps JavaScript API** and **Places API (New)** (`places.googleapis.com`) enabled — the legacy Places API alone is not sufficient after the `AutocompleteSuggestion` / `Place` migration. A `SERVICE_DISABLED` error means Places API (New) is missing on that project. + +--- + +### `titanium-chip` + +**Purpose:** Custom chip (link, filter, or input-chip with remove). + +**Import:** `import '@leavittsoftware/web/titanium/chip/chip.js'` + +| Kind | Name | Type / values | Notes | +| --------- | ----------------------------------------- | ------------- | --------------------- | +| Property | `label` | `string` | | +| Property | `selected`, `disabled`, `non-interactive` | `boolean` | | +| Property | `href`, `download`, `target` | `string` | Link chip | +| Attribute | `input-chip` | `boolean` | Enables remove button | + +**Events:** `remove` (when `input-chip`) + +**Slots:** `icon`, `label`, `trailing` + +**CSS parts:** `button`, `ripple`, `focus-ring` + +**Usage notes / gotchas:** Use `filled` attribute in consuming apps for design consistency + +--- + +### `titanium-chip-multi-select` + +**Purpose:** Filled validator wrapper for slotted chips + add button. + +**Import:** `import '@leavittsoftware/web/titanium/chip-multi-select/chip-multi-select.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------------------------------------------------- | ------------- | ----- | +| Property | `label`, `noItemsText`, `supportingText`, `errorText` | `string` | | +| Property | `required`, `hasItems`, `error`, `resizable`, `disabled` | `boolean` | | + +**Methods:** `checkValidity()`, `reportValidity()`, `reset()` + +**Slots:** default — intended: `md-filled-tonal-button` + `md-input-chip` / chips + +**Usage notes / gotchas:** `disabled` on host does not auto-disable slotted chips/buttons + +--- + +### `titanium-single-select-base` + +**Purpose:** Generic autocomplete single-select base (extended by domain selects). + +**Material Web:** Composes `md-filled-text-field` + `md-menu`; `required` patches inner `md-filled-text-field.checkValidity`. + +**Import:** `import '@leavittsoftware/web/titanium/single-select-base/single-select-base.js'` + +| Kind | Name | Type / values | Notes | +| --------- | ------------------------------------------------------ | -------------------- | ---------------------------------- | +| Property | `label`, `placeholder`, `selected` | | `selected`: `T \| null` | +| Property | `required`, `disabled`, `error`, `errorText` | `boolean` / `string` | | +| Property | `prefixText`, `suffixText`, `supportingText` | `string` | | +| Attribute | `no-asterisk`, `has-leading-icon`, `has-trailing-icon` | `boolean` | | +| Property | `pathToSelectedText` | `string` | Key on selected object for display | +| Property | `positioning`, `match-input-width`, `large`, `shaped` | | Menu positioning | +| Attribute | `disable-menu-open-on-focus` | `boolean` | | +| Attribute | `menu-open` | `boolean` | Reflected | +| State | `isLoading` | `boolean` | | + +**Methods:** `reset()`, `softReset()`, `select()`, `focus()`, `checkValidity()`, `reportValidity()`, `setCustomValidity()`, `trackLoadingPromise()` + +**Events:** `selected`; redispatches menu `opening`/`opened`/`closing`/`closed` + +**Slots:** `leading-icon`, `trailing-icon` + +**CSS parts:** `menu` + +**Usage notes / gotchas:** + +- `required` patches `md-filled-text-field.checkValidity` +- Custom validity when typed but not selected +- `positioning='popover'` falls back to `'fixed'` in Firefox + +--- + +### `titanium-icon-picker` + +**Purpose:** Material Symbols icon search and select. + +**Import:** `import '@leavittsoftware/web/titanium/icon-picker/icon-picker.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------------------------- | ------------- | -------------------------- | +| Property | `favorites` | `string[]` | | +| Property | `whitelist` | `string` | Comma-separated icon names | +| Property | `pathToSelectedText` | `string` | Default `'icon'` | +| + | `TitaniumSingleSelectBase` props | | | + +**Events:** `selected` + +--- + +### `titanium-smart-attachment-input` + +**Purpose:** File upload with chips, image crop, preview, optional delete confirmation. + +**Import:** `import '@leavittsoftware/web/titanium/smart-attachment-input/smart-attachment-input.js'` + +| Kind | Name | Type / values | Notes | +| --------- | ----------------------------------------------------------- | ---------------- | ----------------- | +| Property | `accept`, `multiple`, `required`, `disabled` | | | +| Property | `confirmDelete`, `confirmDeleteHeader`, `confirmDeleteText` | | | +| Property | `addButtonLabel`, `label`, `supportingText`, `noItemsText` | `string` | | +| Property | `options` | `CropperOptions` | Cropper.js config | +| Attribute | `force-png` | `boolean` | | + +**Methods:** `getFiles()`, `setFiles(...)`, `setFilesFromDatabaseAttachments(...)`, `checkValidity()`, `reportValidity()`, `hasChanges()`, `reset()`, `handleNewFile(files)` + +**Events:** `change` + +**Usage notes / gotchas:** Uses internal `titanium-chip-multi-select`; revokes blob URLs on `reset()` + +--- + +### `titanium-filled-input-validator` + +**Purpose:** Filled MdField wrapper with custom `evaluator` validation function. + +**Material Web:** Extends `MdFilledField` — slotted content sits inside the MW field container. + +**Import:** `import '@leavittsoftware/web/titanium/input-validator/filled-input-validator.js'` + +**Source:** `titanium/input-validator/filled-input-validator.ts` + +| Kind | Name | Type / values | Notes | +| -------- | ------------- | ------------------------------------------------------------- | --------------------- | +| Property | `evaluator` | `() => boolean` | Custom validity check | +| Property | `populated` | `boolean` | | +| + | MdField props | `label`, `error`, `error-text`, `supporting-text`, `required` | | + +**Methods:** `checkValidity()`, `reportValidity()`, `reset()` + +**Slots:** default (wraps slotted input) + +--- + +## Dialogs and confirmations + +### `titanium-confirmation-dialog` + +**Purpose:** Imperative promise-based confirm/cancel dialog. + +**Material Web:** Composes `md-dialog` with `md-text-button` / `md-filled-tonal-button` actions. + +**Import:** `import '@leavittsoftware/web/titanium/confirmation-dialog/confirmation-dialog.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------------------------------------------- | ------------- | ----- | +| Property | `headline`, `text` | `string` | | +| Property | `confirmActionText`, `cancelActionText` | `string` | | +| Property | `disableConfirmationAction`, `disableCancelAction` | `boolean` | | + +**Methods:** `open(headline, text)` → `Promise<'cancel' | 'confirmed'>` + +**Slots:** default (content area) + +**CSS parts:** `content-container` + +--- + +### `provide-feedback-dialog` + +**Purpose:** Modal to submit user feedback to Issue Tracking API. + +**Import:** `import '@leavittsoftware/web/leavitt/user-feedback/provide-feedback-dialog.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------- | ----------------------- | ------------ | +| Property | `.userManager` | `AuthZeroLgUserManager` | **Required** | + +**Methods:** `show()`, `reset()` + +**Events:** `PendingStateEvent`, `ShowSnackbarEvent` + +--- + +### `report-a-problem-dialog` + +**Purpose:** Bug report modal with file attachments. + +**Import:** `import '@leavittsoftware/web/leavitt/user-feedback/report-a-problem-dialog.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------- | ----------------------- | ------------ | +| Property | `.userManager` | `AuthZeroLgUserManager` | **Required** | + +**Methods:** `show()`, `reset()` + +**Uses:** `titanium-smart-attachment-input`, `titanium-snackbar-stack` + +--- + +## Feedback, loading, and error pages + +### `titanium-snackbar-stack` + +**Purpose:** Stackable snackbar host; listens for `ShowSnackbarEvent`. + +**Import:** `import '@leavittsoftware/web/titanium/snackbar/snackbar-stack.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------- | ------------------------- | ------------------ | +| Property | `.eventListenerTarget` | `HTMLElement \| Document` | Default `document` | + +**Methods:** `open(message, options?)`, `dismissAll()` + +**Events listened:** `show-snackbar` + +**Usage notes / gotchas:** `display: contents`; place in app shell or dialog (dialogs doing I/O need their own stack) + +--- + +### `titanium-circle-loading-indicator` + +**Purpose:** Scoped circular loading overlay on a pending-state element. + +**Import:** `import '@leavittsoftware/web/titanium/circle-loading-indicator/circle-loading-indicator.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------- | ----------------- | ----- | +| Property | `.pendingStateElement` | `Element \| null` | | + +**Events listened:** `pending-state` + +**Usage notes / gotchas:** + +- Sets parent `inert` while open; 75ms open delay, 400ms min visible +- **Must** dispatch `PendingStateEvent` for work — `isLoading` alone does not drive the overlay +- Calls `stopPropagation()` on the event + +--- + +### `leavitt-error-page` + +**Purpose:** Branded error page with an animated star background; theme-aware. + +**Import:** `import '@leavittsoftware/web/leavitt/error-page/error-page.js'` + +| Kind | Name | Type / values | Notes | +| -------- | --------- | -------------------------- | ------------------ | +| Property | `heading` | `string \| TemplateResult` | Default `'Hmm...'` | +| Property | `message` | `string \| TemplateResult` | | + +**Usage notes / gotchas:** Extends `ThemePreference`; renders a self-contained `` starfield (no external particle dependency) that drifts and twinkles, honors `prefers-reduced-motion`, and recolors on theme change. + +--- + +## Domain selects + +All extend `TitaniumSingleSelectBase` and fire `selected`. All require `.apiService` (`ApiService` instance). + +### `leavitt-company-select` + +**Import:** `import '@leavittsoftware/web/leavitt/company-select/company-select.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ------------------- | -------------------- | --------------------------------- | +| Property | `.apiService` | `ApiService` | **Required** | +| Property | `apiControllerName` | `string` | Default `'Companies'` | +| Property | `companies` | `Partial[]` | Preloaded list | +| Property | `odataParts` | `string[]` | Default `orderby=Name,select=...` | +| Property | `disableAutoLoad` | `boolean` | Skip auto-fetch on `firstUpdated` | + +**Methods:** `reloadCompanies()`, `reset()` (inherited) + +**Usage notes / gotchas:** Local Fuse.js search over preloaded companies; company mark icons in menu items + +--- + +### `leavitt-person-select` + +**Import:** `import '@leavittsoftware/web/leavitt/person-select/person-select.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ------------------------ | ------------------- | ------------------------------------------ | +| Property | `.apiService` | `ApiService` | **Required** | +| Property | `apiControllerName` | `string` | | +| Property | `odataParts` | `string[]` | | +| Property | `searchType` | `local` \| `remote` | Remote uses OData `contains` on `FullName` | +| Property | `enablePeoplePreloading` | `boolean` | | +| Property | `people` | `Partial[]` | | + +--- + +### `leavitt-person-company-select` + +**Import:** `import '@leavittsoftware/web/leavitt/person-company-select/person-company-select.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------------------- | ------------- | ------------ | +| Property | `.apiService` | `ApiService` | **Required** | +| Property | `peopleApiControllerName` | `string` | | +| Property | `companyApiControllerName` | `string` | | + +**Usage notes / gotchas:** Combined people + companies in one autocomplete + +--- + +### `leavitt-person-group-select` + +**Import:** `import '@leavittsoftware/web/leavitt/person-group-select/person-group-select.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ------------------------- | ------------- | ------------ | +| Property | `.apiService` | `ApiService` | **Required** | +| Property | `peopleApiControllerName` | `string` | | +| Property | `groupApiControllerName` | `string` | | + +**Usage notes / gotchas:** Searches people and people groups + +--- + +## File explorer + +### `leavitt-file-explorer` + +**Purpose:** Full file/folder browser with grid/list views, upload, and admin actions. + +**Import:** `import '@leavittsoftware/web/leavitt/file-explorer/file-explorer.js'` + +| Kind | Name | Type / values | Notes | +| --------- | --------------------------- | --------------------------------------------------- | ----------------------------- | +| Property | `.apiService` | `ApiService` | **Required** | +| Attribute | `file-explorer-id` | `number` | | +| Attribute | `folder-id` | `number \| null` | | +| Attribute | `local-storage-display-key` | `string` | Persists grid/list preference | +| Attribute | `prevent-navigation-up` | `boolean` | | +| Property | `display` | `grid` \| `list` | Persisted | +| Property | `state` | `no-permission` \| `files` \| `no-files` \| `error` | | + +**Methods:** `reload()` + +**Events:** `folder-added`, `folder-deleted`, `file-added`, `file-deleted`, `PendingStateEvent`, `ShowSnackbarEvent` + +**Usage notes / gotchas:** + +- Subscribes to `fileExplorerEvents` bus for live modal sync +- Uses `titanium-confirmation-dialog` for deletes +- Internal modals: `leavitt-file-modal`, `leavitt-folder-modal`, `leavitt-add-folder-modal` + +--- + +## Email history + +### `leavitt-email-history-viewer-filled` + +**Purpose:** App-shell integrated email log viewer using `titanium-data-table-core`. + +**Import:** `import '@leavittsoftware/web/leavitt/email-history-viewer/email-history-viewer-filled.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ----------------------------- | ------------- | ------------------- | +| Property | `isActive` | `boolean` | | +| Property | `.apiService` | `ApiService` | **Required** | +| Property | `path`, `apiControllerName` | `string` | | +| Property | `.siteSearchTextFieldContext` | Lit context | Shared search field | + +**Uses:** `titanium-page-control`, filter dialogs, `leavitt-view-sent-email-dialog` + +--- + +## Profile and user feedback + +### `profile-picture` + +**Purpose:** CDN-hosted profile image with optional link and test-user indicator. + +**Import:** `import '@leavittsoftware/web/leavitt/profile-picture/profile-picture.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ---------------------------------------------------------------- | -------------------- | -------------- | +| Property | `fileName` | `string` | CDN filename | +| Property | `shape` | `circle` \| `square` | | +| Property | `size` | `number` | Pixels | +| Property | `show-ring`, `show-test-user-indicator`, `useIntrinsicImageSize` | `boolean` | | +| Property | `profile-picture-link-person-id` | `number` | Directory link | + +**CSS parts:** `test-user-indicator` + +--- + +### `profile-picture-menu` + +**Purpose:** User avatar + account menu popover. + +**Import:** `import '@leavittsoftware/web/leavitt/profile-picture/profile-picture-menu.js'` + +| Kind | Name | Type / values | Notes | +| -------- | ------------------------------------------------------------------------ | ----------------------- | ----- | +| Property | `.userManager` | `AuthZeroLgUserManager` | | +| Property | `size`, `profilePictureFileName`, `personId`, `email`, `company`, `name` | | | +| Property | `positioning` | `popover` \| `fixed` | | + +**Slots:** `content` + +**Usage notes / gotchas:** Auto-syncs from `userManager.onIdentityUpdated`; opens auth if no `personId` + +--- + +### `titanium-profile-picture-stack` + +**Purpose:** Overlapping profile picture stack with overflow count. + +**Import:** `import '@leavittsoftware/web/titanium/profile-picture-stack/profile-picture-stack.js'` + +| Kind | Name | Type / values | Notes | +| -------- | -------------------------------------------------------- | ------------------- | ----------- | +| Property | `people` | `Partial[]` | | +| Property | `max` | `number` | Max visible | +| Property | `size`, `overlap` | `number` | | +| Property | `enable-directory-href`, `show-full-name`, `auto-resize` | `boolean` | | + +**CSS parts:** `additional-users`, `additional-users-paragraph`, `name`, `profile-picture` + +**Uses:** `profile-picture` internally + +--- + +## Service worker notifier + +### `leavitt-service-worker-notifier` + +**Purpose:** Themed full-screen popover prompting reload after SW update. + +**Import:** `import '@leavittsoftware/web/leavitt/service-worker-notifier/service-worker-notifier.js'` + +**Usage notes / gotchas:** Extends `ThemePreference`; auto-registers SW update listeners; click anywhere reloads + +--- + +# Shared utilities + +## Events + +| Symbol | Import path | Event name | Payload / usage | +| ------------------------------ | ------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------- | +| `ShowSnackbarEvent` | `titanium/snackbar/show-snackbar-event` | `show-snackbar` | `message: string \| Partial`, optional `SnackbarOptions` | +| `PendingStateEvent` | `titanium/types/pending-state-event` | `pending-state` | `{ promise: Promise }`; bubbles, composed | +| `DateRangeChangedEvent` | `titanium/date-range-selector/date-range-change-event` | `date-range-changed` | Class exists; `titanium-date-range-selector` fires `change` instead | +| `ThemePreferenceEvent` | `leavitt/theme/theme-preference-event` | theme preference changes | Subscribe for dark/light switches | +| `DataTableItemsReorderedEvent` | `titanium/data-table/data-table-core` | `titanium-data-table-items-reorder` | Exported from data-table-core; core dispatches `items-reordered` on apply | + +## Services + +### `ApiService` + +**Import:** `leavitt/api-service/api-service` + +| Method | Notes | +| ------------------------------------------------- | -------------------------- | +| `getAsync(urlPath)` | Returns `ODataResponse` | +| `postAsync(urlPath, body?)` | JSON POST | +| `putAsync`, `patchAsync`, `deleteAsync` | Standard verbs | +| `uploadFile(urlPath, file, onprogress, options?)` | XHR upload with progress | +| `aggregateResponses(promises)` | Batch multiple async ops | +| `addHeader(key, value)` | e.g. `X-LGAppName` | + +**Response helpers:** `ODataResponse.toList()`, `.odataCount` + +**Related:** `HttpError`, `BearerTokenProvider`, `objectToFormData`, `BlobResponse` + +### `AuthZeroLgUserManager` + +**Import:** `leavitt/user-manager/auth-zero-lg-user-manager` + +Auth0 integration for `profile-picture-menu`, feedback dialogs. Provides `identity`, `authenticate()`, `onIdentityUpdated`. + +## Controllers and buses + +| Symbol | Import path | Purpose | +| --------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------ | +| `FilterController` | `titanium/data-table/filter-controller` | URL query-string filter state for list pages | +| `TitaniumSiteSearchTextFieldController` | `titanium/site-search-text-field-controller/site-search-text-field-controller` | App-level shared search field via Lit context | +| `EventBus` | `titanium/event-bus/event-bus` | Typed pub/sub (`subscribe`, `dispatch`, `unsubscribe`) | +| `fileExplorerEvents` | `leavitt/file-explorer/events/file-explorer-events` | File explorer modal sync bus | + +## Contexts + +| Context | Import path | +| -------------------------------- | ------------------------------------------------------------------------------ | +| `mainMenuPositionContext` | `leavitt/app/contexts/main-menu-position-context` | +| `TitaniumTextFieldSearchContext` | `titanium/site-search-text-field-controller/site-search-text-field-controller` | + +## Mixins and decorators + +| Symbol | Import path | Purpose | +| --------------------- | ---------------------------------------- | ------------------------------------------------------------ | +| `promiseTracking` | `titanium/helpers/promise-tracking` | `@promiseTracking('methodName')` decorator for loading flags | +| `PendingStateCatcher` | `titanium/helpers/pending-state-catcher` | Mixin to catch pending-state on host | +| `ThemePreference` | `leavitt/theme/theme-preference` | Dark/light theme mixin | + +These apply to the **host class** (`LitElement` or `Md*` subclass). They stack with Material Web properties on the same element when the component extends an `Md*` class, or apply to the wrapper when the component composes inner `md-*` children (see **Material Web foundation**). + +## Helpers (selected) + +| Symbol | Import path | Purpose | +| ----------------------------------------------- | --------------------------------------------------------------- | -------------------------- | +| `Debouncer` | `titanium/helpers/debouncer` | Debounced async calls | +| `delay` | `titanium/helpers/delay` | Promise delay | +| `getCdnDownloadUrl`, `getCdnInlineUrl` | `titanium/helpers/get-cdn-download-url`, `get-cdn-Inline-url` | CDN attachment URLs | +| `getCompanyMarkUrl`, `getCompanyLogoUrl` | `titanium/helpers/get-company-mark-url`, `get-company-logo-url` | Company branding URLs | +| `formatAddress`, address utils | `titanium/helpers/address/*` | Address formatting | +| Phone formatters | `titanium/helpers/phone-numbers/*` | Phone number display | +| `convertArrayToCsv`, `startCsvDownload` | `titanium/helpers/csv/*` | CSV export | +| `getSearchTokens` | `titanium/helpers/get-search-token` | OData search token parsing | +| `escapeTerm` | `titanium/helpers/escape-term` | OData string escaping | +| `groupBy`, `join`, `middleEllipsis` | `titanium/helpers/*` | General utilities | +| `notNull`, `notUndefined`, `notNullOrUndefined` | `titanium/helpers/*` | Type guards | +| `installMediaQueryWatcher` | `titanium/helpers/install-media-query-watcher` | Responsive layout callback | +| `findScrollableParent` | `titanium/helpers/find-scrollable-parent` | Scroll container detection | +| `isDevelopment` | `titanium/helpers/is-development` | Dev environment detection | + +## Hacks + +| Module | Import path | When to use | +| --------------------------- | ----------------------------------------- | ------------------------------------------------------- | +| `dialogCloseNavigationHack` | `titanium/hacks/dialog-navigation-hack` | Close dialog on SPA navigation without breaking history | +| `dialogZindexHack` | `titanium/hacks/dialog-zindex-hack` | Stacking context issues with nested dialogs | +| `dialogOverflowHacks` | `titanium/hacks/dialog-overflow-hacks` | Body scroll lock with dialogs | +| `reportValidityIfError` | `titanium/hacks/report-validity-if-error` | Form validation helper | diff --git a/packages/web/leavitt/api-service/api-service.ts b/packages/web/leavitt/api-service/api-service.ts index 91ff88a75..c9a93bd80 100644 --- a/packages/web/leavitt/api-service/api-service.ts +++ b/packages/web/leavitt/api-service/api-service.ts @@ -121,7 +121,7 @@ export default class ApiService { xhr.send(file); }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'UPLOAD', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'UPLOAD', urlPath)); } } @@ -157,7 +157,7 @@ export default class ApiService { signal: options?.abortController?.signal, }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'POST', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'POST', urlPath)); } if (options?.responseType === 'blob') { @@ -195,7 +195,7 @@ export default class ApiService { signal: options?.abortController?.signal, }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'PATCH', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'PATCH', urlPath)); } if (options?.responseType === 'blob') { @@ -233,7 +233,7 @@ export default class ApiService { signal: options?.abortController?.signal, }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'PATCH', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'PATCH', urlPath)); } if (options?.responseType === 'blob') { @@ -257,7 +257,7 @@ export default class ApiService { try { response = await fetch(this.#getFullUri(urlPath), { method: 'DELETE', headers: headers, signal: options?.abortController?.signal }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'DELETE', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'DELETE', urlPath)); } if (options?.responseType === 'blob') { @@ -285,7 +285,7 @@ export default class ApiService { signal: options?.abortController?.signal, }); } catch (error) { - return Promise.reject(this.#rewriteFetchErrors(error, 'GET', urlPath)); + return Promise.reject(this.#rewriteFetchErrors(error as { name: string; message: string } | AbortError, 'GET', urlPath)); } if (options?.responseType === 'blob') { @@ -307,8 +307,9 @@ export default class ApiService { try { await call(); } catch (httpError) { - httpErrors.push(httpError); - const errorMsg = httpError.message; + const error = httpError as HttpError; + httpErrors.push(error); + const errorMsg = error.message; errorMessageToCount.set(errorMsg, (errorMessageToCount.get(errorMsg) ?? 0) + 1); } }); diff --git a/packages/web/leavitt/api-service/blob-response.ts b/packages/web/leavitt/api-service/blob-response.ts index f387eeba4..71a9f12e1 100644 --- a/packages/web/leavitt/api-service/blob-response.ts +++ b/packages/web/leavitt/api-service/blob-response.ts @@ -1,7 +1,7 @@ export class BlobResponse { public readonly status: number; public readonly headers: Headers; - public readonly metadata: Map; + public readonly metadata: Map = new Map(); public readonly blob: Blob; constructor(response: Response, blob: Blob) { diff --git a/packages/web/leavitt/app/app-logo.ts b/packages/web/leavitt/app/app-logo.ts index 84342f388..310a6ee4e 100644 --- a/packages/web/leavitt/app/app-logo.ts +++ b/packages/web/leavitt/app/app-logo.ts @@ -7,7 +7,7 @@ import { ThemePreference } from '@leavittsoftware/web/leavitt/theme/theme-prefer export class LeavittAppLogo extends ThemePreference(LitElement) { @property({ type: String }) accessor href: string = '/'; @property({ type: String }) accessor title: string = 'Back to home'; - @property({ type: String, attribute: 'app-name' }) accessor appName: string | null; + @property({ type: String, attribute: 'app-name' }) accessor appName: string | null = null; static styles = [ css` diff --git a/packages/web/leavitt/app/app-main-content-container.ts b/packages/web/leavitt/app/app-main-content-container.ts index ee30843e4..339b6899e 100644 --- a/packages/web/leavitt/app/app-main-content-container.ts +++ b/packages/web/leavitt/app/app-main-content-container.ts @@ -7,11 +7,11 @@ import { consume } from '@lit/context'; @customElement('leavitt-app-main-content-container') export class LeavittAppContentContainer extends LitElement { - @query('scroll-container') accessor scrollContainer: HTMLDivElement | null; - @property({ type: Object }) private accessor pendingStateElement: Element | null; + @query('scroll-container') accessor scrollContainer!: HTMLDivElement | null; + @property({ type: Object }) private accessor pendingStateElement: Element | null = null; @consume({ context: mainMenuPositionContext, subscribe: true }) @property({ type: String, reflect: true, attribute: 'main-menu-position' }) - public mainMenuPosition: string; + public mainMenuPosition: string = 'full'; static styles = [ css` diff --git a/packages/web/leavitt/app/app-navigation-header.ts b/packages/web/leavitt/app/app-navigation-header.ts index b860702e7..95bbe07fd 100644 --- a/packages/web/leavitt/app/app-navigation-header.ts +++ b/packages/web/leavitt/app/app-navigation-header.ts @@ -11,20 +11,20 @@ export class LeavittAppNavigationHeader extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'sticky-top' }) accessor stickyTop: boolean = false; @property({ type: Object, attribute: 'scrollable-parent' }) accessor scrollableParent: Element | null = null; - @property({ type: String }) accessor level1Text: string | null; - @property({ type: String }) accessor level1Href: string | null; + @property({ type: String }) accessor level1Text: string | null = null; + @property({ type: String }) accessor level1Href: string | null = null; - @property({ type: String }) accessor level2Text: string | null; - @property({ type: String }) accessor level2Href: string | null; + @property({ type: String }) accessor level2Text: string | null = null; + @property({ type: String }) accessor level2Href: string | null = null; - @property({ type: String }) accessor level3Text: string | null; - @property({ type: String }) accessor level3Href: string | null; + @property({ type: String }) accessor level3Text: string | null = null; + @property({ type: String }) accessor level3Href: string | null = null; - @property({ type: String }) accessor level4Text: string | null; - @property({ type: String }) accessor level4Href: string | null; + @property({ type: String }) accessor level4Text: string | null = null; + @property({ type: String }) accessor level4Href: string | null = null; - @property({ type: String }) accessor level5Text: string | null; - @property({ type: String }) accessor level5Href: string | null; + @property({ type: String }) accessor level5Text: string | null = null; + @property({ type: String }) accessor level5Href: string | null = null; @property({ type: Boolean, reflect: true, attribute: 'is-scrolled' }) private accessor isScrolled: boolean = false; #scrollableParent: Element | null = null; diff --git a/packages/web/leavitt/company-select/company-select.ts b/packages/web/leavitt/company-select/company-select.ts index 786def96c..c4b8a8faf 100644 --- a/packages/web/leavitt/company-select/company-select.ts +++ b/packages/web/leavitt/company-select/company-select.ts @@ -15,6 +15,7 @@ import { TitaniumSingleSelectBase } from '../../titanium/single-select-base/sing import { Debouncer } from '../../titanium/helpers/debouncer'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; import { getCompanyMarkUrl } from '@leavittsoftware/web/titanium/helpers/get-company-mark-url'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; /** * Single select input that searches Leavitt Group companies @@ -37,7 +38,7 @@ export class LeavittCompanySelect extends TitaniumSingleSelectBase) => { - const theme = !this.shaped || !this.filled ? this.themePreference : this.shaped && this.filled && this.themePreference === 'dark' ? 'light' : 'dark'; + const theme = !this.shaped ? this.themePreference : this.shaped && this.themePreference === 'dark' ? 'light' : 'dark'; return html` ${company.Name} @@ -98,11 +99,11 @@ export class LeavittCompanySelect extends TitaniumSingleSelectBase>(`${this.apiControllerName}?${this.odataParts.join('&')}`); - this.loadWhile(get); + this.trackLoadingPromise(get); const result = await get; return result?.toList() ?? []; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return []; } diff --git a/packages/web/leavitt/email-history-viewer/email-history-view-list-filter-dialog.ts b/packages/web/leavitt/email-history-viewer/email-history-view-list-filter-dialog.ts deleted file mode 100644 index 45619a04f..000000000 --- a/packages/web/leavitt/email-history-viewer/email-history-view-list-filter-dialog.ts +++ /dev/null @@ -1,221 +0,0 @@ -import '../../titanium/date-range-selector/date-range-selector'; - -import '@material/web/dialog/dialog'; -import '@material/web/button/text-button'; -import '@material/web/icon/icon'; -import '@material/web/chips/input-chip'; - -import { dialogZIndexHack } from '../../titanium/hacks/dialog-zindex-hack'; -import { LitElement, PropertyValues, css, html } from 'lit'; -import { customElement, property, query, state } from 'lit/decorators.js'; -import { MdDialog } from '@material/web/dialog/dialog'; -import { LoadWhile } from '../../titanium/helpers/load-while'; -import { FilterController } from '../../titanium/data-table/filter-controller'; -import { rangeLabel } from '../../titanium/date-range-selector/types/range-label'; -import { TitaniumDateRangeSelector } from '../../titanium/date-range-selector/date-range-selector'; -import { DateRangeKey } from '../../titanium/date-range-selector/types/date-range-key'; -import { DateRanges } from '../../titanium/date-range-selector/types/date-ranges'; -import { DOMEvent } from '../../titanium/types/dom-event'; -import { dialogCloseNavigationHack, dialogOpenNavigationHack } from '../../titanium/hacks/dialog-navigation-hack'; -import ApiService from '../api-service/api-service'; -import { EmailTemplate } from '@leavittsoftware/lg-core-typescript'; -import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; -import { MdOutlinedSelect } from '@material/web/select/outlined-select'; - -export type FilterKeys = 'template' | 'startDate' | 'endDate' | 'dateRange'; - -@customElement('leavitt-email-history-view-list-filter-dialog') -export class LeavittEmailHistoryViewListFilterDialog extends LoadWhile(LitElement) { - @property({ type: Boolean }) accessor isActive: boolean; - @property({ type: Object }) accessor apiService: ApiService | null; - - @state() private accessor filterController: FilterController; - @state() private accessor template: Partial[] = []; - @state() private accessor templateId: string; - - #templatesAreDirty = true; - - //Date range props - @state() private accessor startDate: string; - @state() private accessor endDate: string; - - @query('md-dialog') private accessor dialog!: MdDialog; - @query('titanium-date-range-selector') private accessor dateRangeSelect!: TitaniumDateRangeSelector; - - async firstUpdated() { - this.filterController.subscribeToFilterChange(async () => { - this.#preloadChipData(); - this.requestUpdate('filterController'); - }); - } - - async updated(changedProps: PropertyValues) { - if (this.isActive && changedProps.has('isActive')) { - this.#preloadChipData(); - } - } - - async #preloadChipData() { - //Preload for chips - if (this.filterController.getValue('template') && this.#templatesAreDirty) { - this.template = await this.#getTemplatesAsync(); - } - } - - async #getTemplatesAsync() { - if (!this.apiService) { - console.warn('No api service provided'); - return []; - } - - const odataParts = ['select=Id,Name,IsExpired', 'orderby=Name']; - - try { - const get = this.apiService.getAsync(`EmailTemplates?${odataParts.join('&')}`); - this.loadWhile(get); - const entities = (await get).toList(); - this.#templatesAreDirty = false; - return entities; - } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); - } - return []; - } - - public async open() { - if (this.#templatesAreDirty) { - this.template = await this.#getTemplatesAsync(); - } - - this.templateId = this.filterController.getValue('template') ?? ''; - - //populate date range - const dateRange = this.filterController.getValue('dateRange') as DateRangeKey; - this.startDate = (dateRange === 'custom' ? this.filterController.getValue('startDate') : DateRanges.get(dateRange)?.startDate()) || ''; - this.endDate = (dateRange === 'custom' ? this.filterController.getValue('endDate') : DateRanges.get(dateRange)?.endDate()) || ''; - - this.dialog.show(); - } - - static styles = [ - css` - :host { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - } - - md-dialog { - max-width: 550px; - width: calc(100vw - 24px); - - div[inactive] { - font-size: 12px; - line-height: 14px; - opacity: 0.8; - } - - md-outlined-select { - width: 100%; - margin-top: 24px; - } - } - - [hidden] { - display: none !important; - } - `, - ]; - - render() { - return html` - { - e.preventDefault(); - this.filterController.setValue('template', null); - }} - > - content_copy - - - { - e.preventDefault(); - this.filterController.setValue('dateRange', null); - this.filterController.setValue('startDate', null); - this.filterController.setValue('endDate', null); - }} - > - date_range - - - ) => { - dialogZIndexHack(e.target); - dialogOpenNavigationHack(e.target); - }} - @close=${(e: DOMEvent) => { - dialogCloseNavigationHack(e.target); - }} - > -
Filter logs by
-
- ) => { - this.startDate = event.target.startDate || ''; - this.endDate = event.target.endDate || ''; - }} - > - - ) => (this.templateId = e.target.value)} - > - content_copy - - ${this.template.map( - (o) => - html` -
${o.Name}
- ${o.IsExpired ? html`
Inactive
` : ''} - content_copy -
` - )} -
-
-
- this.dialog.close('cancel')}> Close - { - this.filterController.setValue('template', this.templateId || null); - - //set date range - this.filterController.setValue('dateRange', this.dateRangeSelect.range === 'allTime' ? null : this.dateRangeSelect.range); - this.filterController.setValue('startDate', this.dateRangeSelect.range === 'custom' ? this.startDate || null : null); - this.filterController.setValue('endDate', this.dateRangeSelect.range === 'custom' ? this.endDate || null : null); - - this.dialog.close('apply'); - }} - >Apply -
-
- `; - } -} diff --git a/packages/web/leavitt/email-history-viewer/email-history-viewer-filled-filter-dialog.ts b/packages/web/leavitt/email-history-viewer/email-history-viewer-filled-filter-dialog.ts index 0237b64b3..4c3f36eb7 100644 --- a/packages/web/leavitt/email-history-viewer/email-history-viewer-filled-filter-dialog.ts +++ b/packages/web/leavitt/email-history-viewer/email-history-viewer-filled-filter-dialog.ts @@ -12,7 +12,7 @@ import { dialogZIndexHack } from '../../titanium/hacks/dialog-zindex-hack'; import { LitElement, PropertyValues, css, html } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { MdDialog } from '@material/web/dialog/dialog'; -import { LoadWhile } from '../../titanium/helpers/load-while'; +import { promiseTracking } from '../../titanium/helpers/promise-tracking'; import { FilterController } from '../../titanium/data-table/filter-controller'; import { rangeLabel } from '../../titanium/date-range-selector/types/range-label'; import { TitaniumDateRangeSelector } from '../../titanium/date-range-selector/date-range-selector'; @@ -24,23 +24,29 @@ import ApiService from '../api-service/api-service'; import { EmailTemplate } from '@leavittsoftware/lg-core-typescript'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; import { MdOutlinedSelect } from '@material/web/select/outlined-select'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type FilterKeys = 'template' | 'startDate' | 'endDate' | 'dateRange'; @customElement('leavitt-email-history-viewer-filled-filter-dialog') -export class LeavittEmailHistoryViewerFilledFilterDialog extends LoadWhile(LitElement) { - @property({ type: Boolean }) accessor isActive: boolean; - @property({ type: Object }) accessor apiService: ApiService | null; +export class LeavittEmailHistoryViewerFilledFilterDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; - @state() private accessor filterController: FilterController; + @property({ type: Boolean }) accessor isActive: boolean = false; + @property({ type: Object }) accessor apiService: ApiService | null = null; + + @state() private accessor filterController!: FilterController; @state() private accessor template: Partial[] = []; - @state() private accessor templateId: string; + @state() private accessor templateId: string = ''; #templatesAreDirty = true; //Date range props - @state() private accessor startDate: string; - @state() private accessor endDate: string; + @state() private accessor startDate: string = ''; + @state() private accessor endDate: string = ''; @query('md-dialog') private accessor dialog!: MdDialog; @query('titanium-date-range-selector') private accessor dateRangeSelect!: TitaniumDateRangeSelector; @@ -75,12 +81,12 @@ export class LeavittEmailHistoryViewerFilledFilterDialog extends LoadWhile(LitEl try { const get = this.apiService.getAsync(`EmailTemplates?${odataParts.join('&')}`); - this.loadWhile(get); + this.trackLoadingPromise(get); const entities = (await get).toList(); this.#templatesAreDirty = false; return entities; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return []; } @@ -187,7 +193,6 @@ export class LeavittEmailHistoryViewerFilledFilterDialog extends LoadWhile(LitEl
Filter logs by
; @customElement('leavitt-email-history-viewer-filled') -export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElement) { - @property({ type: Boolean }) public accessor isActive: boolean; - @property({ type: Object }) public accessor apiService: ApiService | null; - @property({ type: String }) public accessor path: string; - @property({ type: Object }) public accessor siteSearchTextFieldContext: TitaniumTextFieldSearchContext; +export default class LeavittEmailHistoryViewerFilled extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + + @property({ type: Boolean }) public accessor isActive: boolean = false; + @property({ type: Object }) public accessor apiService: ApiService | null = null; + @property({ type: String }) public accessor path: string = ''; + @property({ type: Object }) public accessor siteSearchTextFieldContext!: TitaniumTextFieldSearchContext; @property({ type: String }) accessor apiControllerName: string = 'EmailTemplateLogs'; - /** - * @deprecated use the siteSearchTextFieldController + siteSearchTextFieldContext instead - */ - @property({ type: String }) public accessor toolbarSearchTerm: string = ''; - - @state() public accessor searchTerm: string = ''; - // Data table props @state() private accessor items: Array = []; @state() private accessor selected: Array = []; @@ -122,10 +120,10 @@ export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElemen @query('titanium-data-table-core') private accessor dataTable!: TitaniumDataTableCore; @query('leavitt-email-history-viewer-filled-filter-dialog') private accessor filterDialog!: LeavittEmailHistoryViewerFilledFilterDialog; - @query('titanium-page-control') private accessor pageControl: TitaniumPageControl | null; - @query('leavitt-app-main-content-container') private accessor mainContentContainer: LeavittAppContentContainer | null; - @query('leavitt-view-sent-email-dialog') private accessor viewDialog: LeavittViewSentEmailDialog | null; - @query('leavitt-view-email-template-info-dialog') private accessor viewEmailTemplateInfoDialog: LeavittViewEmailTemplateInfoDialog | null; + @query('titanium-page-control') private accessor pageControl!: TitaniumPageControl | null; + @query('leavitt-app-main-content-container') private accessor mainContentContainer!: LeavittAppContentContainer | null; + @query('leavitt-view-sent-email-dialog') private accessor viewDialog!: LeavittViewSentEmailDialog | null; + @query('leavitt-view-email-template-info-dialog') private accessor viewEmailTemplateInfoDialog!: LeavittViewEmailTemplateInfoDialog | null; searchController: TitaniumSiteSearchTextFieldController | undefined; @@ -155,14 +153,6 @@ export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElemen this.#reload(); } - if (this.isActive && changedProps.has('toolbarSearchTerm') && this.searchTerm !== this.toolbarSearchTerm) { - this.searchTerm = this.toolbarSearchTerm; - if (this.pageControl) { - this.pageControl.page = 0; - } - this.#doSearchDebouncer.debounce(); - } - if (changedProps.has('path')) { this.filterController.path = this.path; } @@ -192,15 +182,11 @@ export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElemen } async #reload() { - const { items, odataCount } = await this.#getItemsAsync( - this.siteSearchTextFieldContext ? (this.searchController?.searchTerm ?? null) : (this.searchTerm ?? null) - ); + const { items, odataCount } = await this.#getItemsAsync(this.searchController?.searchTerm ?? null); this.items = items; this.resultTotal = odataCount; } - #doSearchDebouncer = new Debouncer(() => this.#reload()); - renderRecipients(recipients: string | null, maxRecipients: number = 1) { const recipientsList = recipients @@ -270,13 +256,13 @@ export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElemen } const get = this.apiService?.getAsync(`${this.apiControllerName}/?${odataParts.join('&')}`); - this.loadWhile(get); - this.dataTable.loadWhile(get); + this.trackLoadingPromise(get); + this.dataTable.trackLoadingPromise(get); this.dispatchEvent(new PendingStateEvent(get)); const result = await get; return { items: result.toList(), odataCount: result.odataCount }; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error, { autoHide: 7500 })); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial, { autoHide: 7500 })); } return { items: [], odataCount: 0 }; } @@ -365,7 +351,6 @@ export default class LeavittEmailHistoryViewerFilled extends LoadWhile(LitElemen > = []; - @state() private accessor selected: Array> = []; - @state() private accessor searchTerm: string = ''; - @state() private accessor resultTotal: number = 0; - @state() private accessor sortDirection: '' | 'asc' | 'desc' = 'desc'; - @state() private accessor sortBy: string = 'SentDate'; - @state() private accessor filterController: FilterController; - - @query('titanium-data-table') private accessor dataTable!: TitaniumDataTable; - @query('leavitt-view-sent-email-dialog') private accessor viewDialog!: LeavittViewSentEmailDialog; - @query('leavitt-email-history-view-list-filter-dialog') private accessor filterModal: LeavittEmailHistoryViewListFilterDialog; - @query('leavitt-view-email-template-info-dialog') private accessor viewEmailTemplateInfoDialog!: LeavittViewEmailTemplateInfoDialog; - - #isDirty: boolean = true; - - constructor() { - super(); - this.filterController = new FilterController(''); - this.filterController.setFilter('dateRange', () => ''); - this.filterController.setFilter('startDate', () => ''); - this.filterController.setFilter('endDate', () => ''); - this.filterController.setFilter('template', (val) => `EmailTemplateId eq ${val}`); - - this.filterController.subscribeToFilterChange(async () => { - if (this.isActive) { - this.dataTable.resetPage(); - this.#reload(); - } else { - this.#isDirty = true; - } - }); - this.filterController.loadFromQueryString(); - } - - updated(changedProps: PropertyValues) { - if (this.isActive && (changedProps.has('isActive') || changedProps.has('apiControllerName') || this.#isDirty)) { - this.#reload(); - } - - if (changedProps.has('path')) { - this.filterController.path = this.path; - } - } - - #reload() { - this.#getLogsAsync(this.searchTerm); - } - - #onSortDirectionChange(e: CustomEvent<'' | 'asc' | 'desc'>) { - this.sortDirection = e.detail; - this.dataTable.resetPage(); - this.#reload(); - } - - #onSortByChange(e: CustomEvent) { - this.sortBy = e.detail; - this.dataTable.resetPage(); - this.#reload(); - } - - #doSearchDebouncer = new Debouncer((searchTerm: string) => this.#getLogsAsync(searchTerm)); - - renderRecipients(recipients: string | null, maxRecipients: number = 1) { - const recipientsList = - recipients - ?.split(',') - .filter((o) => !!o) - .map((o) => o.trim()) - .reverse() ?? []; - - if (recipientsList?.length > maxRecipients) { - return html`${repeat( - recipientsList.slice(0, maxRecipients), - (o) => o, - (o) => html`${o}
` - )} ${recipientsList.length - maxRecipients} more... `; - } - return repeat( - recipientsList, - (o) => o, - (o) => html`${o}
` - ); - } - - async #getLogsAsync(searchTerm: string) { - if (!this.apiService) { - console.warn('No api service provided'); - return; - } - - let filterParts: string[] = []; - const searchTokens = getSearchTokens(searchTerm); - const searchFilter = searchTokens.map((token: string) => `(contains(Subject, '${token}') or contains(Recipients, '${token}'))`).join(' and '); - if (searchTokens.length > 0) { - filterParts.push(`${searchFilter}`); - } - - //Date filters - const dateRange = this.filterController.getValue('dateRange') as DateRangeKey; - const startDate = dateRange === 'custom' ? this.filterController.getValue('startDate') : DateRanges.get(dateRange)?.startDate(); - const endDate = dateRange === 'custom' ? this.filterController.getValue('endDate') : DateRanges.get(dateRange)?.endDate(); - if (startDate) { - filterParts.push(`SentDate ge ${dayjs(startDate).format('YYYY-MM-DD')}`); - } - if (endDate) { - filterParts.push(`SentDate le ${dayjs(endDate).format('YYYY-MM-DD')}`); - } - if (!isDevelopment) { - filterParts.push('IsTestMessage eq false'); - } - - filterParts = [...filterParts, ...this.filterController.getActiveFilterOdata()]; - - const odataParts = [ - `select=Id,Recipients,SentDate,Subject${isDevelopment ? ',IsTestMessage' : ''}`, - 'expand=EmailTemplate(select=Id,Name,IsExpired)', - `top=${await this.dataTable.getTake()}`, - `orderby=${this.sortBy} ${this.sortDirection}`, - `skip=${(await this.dataTable.getTake()) * (await this.dataTable.getPage())}`, - 'count=true', - ]; - if (filterParts.length > 0) { - odataParts.push(`filter=${filterParts.join(' and ')}`); - } - try { - const get = this.apiService.getAsync>(`${this.apiControllerName}/?${odataParts.join('&')}`); - this.dataTable.loadWhile(get); - this.loadWhile(get); - const result = await get; - this.resultTotal = result.odataCount; - this.logs = result.toList(); - } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); - } finally { - this.#isDirty = false; - } - } - - static styles = [ - ellipsis, - a, - css` - :host { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 24px; - } - - header { - display: grid; - gap: 12px; - md-text-button { - justify-self: center; - } - } - - [inactive], - span[time], - span[more] { - font-size: 12px; - line-height: 14px; - opacity: 0.8; - } - - md-filled-tonal-icon-button { - --md-filled-tonal-icon-button-container-height: 32px; - --md-filled-tonal-icon-button-icon-size: 21px; - } - - md-text-button { - text-wrap: auto; - } - - [hidden] { - display: none !important; - } - `, - ]; - - render() { - return html` -
- - - this.viewEmailTemplateInfoDialog.open()}> - chat_info - What emails does this tool send? - -
- >>) => { - this.selected = [...e.detail]; - }} - @paging-changed=${() => this.#reload()} - .count=${this.resultTotal} - .items=${this.logs} - .searchTerm=${this.searchTerm} - > - ) => { - this.searchTerm = e.target.value; - this.dataTable.resetPage(); - this.#doSearchDebouncer.debounce(this.searchTerm); - }} - > - - - - this.filterModal.open()}> - filter_list - - - - - - - - - - - ${isDevelopment - ? html`` - : nothing} - - - ${repeat( - this.logs ?? [], - (item) => item.Id, - (item) => html` - - ${item.SentDate - ? html`${dayjs(item.SentDate).format('MMM DD, YY')}
${dayjs(item.SentDate).format('h:mm A')}` - : '-'}
- ${item.Subject ?? '-'} - ${this.renderRecipients(item.Recipients ?? null)} - -
${item.EmailTemplate?.Name}
- ${item.EmailTemplate?.IsExpired ? html`
Inactive
` : ''}
- ${isDevelopment ? html`
${item.IsTestMessage ? 'Yes' : 'No'}
` : nothing} - this.viewDialog.open(item.Id ?? 0)}>pageview - -
- ` - )} -
- - - `; - } -} diff --git a/packages/web/leavitt/email-history-viewer/view-email-template-info-dialog.ts b/packages/web/leavitt/email-history-viewer/view-email-template-info-dialog.ts index 825a9dcc9..07f08811e 100644 --- a/packages/web/leavitt/email-history-viewer/view-email-template-info-dialog.ts +++ b/packages/web/leavitt/email-history-viewer/view-email-template-info-dialog.ts @@ -6,7 +6,7 @@ import '@material/web/progress/circular-progress'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { EmailTemplate } from '@leavittsoftware/lg-core-typescript'; -import { LoadWhile } from '../../titanium/helpers/load-while'; +import { promiseTracking } from '../../titanium/helpers/promise-tracking'; import { MdDialog } from '@material/web/dialog/dialog'; import { DOMEvent } from '../../titanium/types/dom-event'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; @@ -17,19 +17,25 @@ import { SnackbarStack } from '../../titanium/snackbar/snackbar-stack'; import { p } from '../../titanium/styles/p'; import { repeat } from 'lit/directives/repeat.js'; import { h2 } from '../../titanium/styles/h2'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type CloseReason = 'done'; @customElement('leavitt-view-email-template-info-dialog') -export class LeavittViewEmailTemplateInfoDialog extends LoadWhile(LitElement) { - @property({ type: Object }) accessor apiService: ApiService | null; +export class LeavittViewEmailTemplateInfoDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; - @state() private accessor emailTemplates: Partial[] | null; + @property({ type: Object }) accessor apiService: ApiService | null = null; + + @state() private accessor emailTemplates: Partial[] | null = null; @query('titanium-snackbar-stack') private accessor snackbar!: SnackbarStack; - @query('md-dialog') private accessor dialog: MdDialog; + @query('md-dialog') private accessor dialog!: MdDialog; - #resolve: (value: CloseReason) => void; + #resolve!: (value: CloseReason) => void; async open() { this.emailTemplates = []; this.dialog.returnValue = ''; @@ -55,11 +61,11 @@ export class LeavittViewEmailTemplateInfoDialog extends LoadWhile(LitElement) { try { const get = this.apiService.getAsync>(`EmailTemplates?${odataParts.join('&')}`); - this.loadWhile(get); + this.trackLoadingPromise(get); const result = await get; return result?.entities; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return []; } diff --git a/packages/web/leavitt/email-history-viewer/view-sent-email-dialog.ts b/packages/web/leavitt/email-history-viewer/view-sent-email-dialog.ts index 7c9d23007..bd9985df0 100644 --- a/packages/web/leavitt/email-history-viewer/view-sent-email-dialog.ts +++ b/packages/web/leavitt/email-history-viewer/view-sent-email-dialog.ts @@ -10,7 +10,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { EmailTemplateLog } from '@leavittsoftware/lg-core-typescript'; -import { LoadWhile } from '../../titanium/helpers/load-while'; +import { promiseTracking } from '../../titanium/helpers/promise-tracking'; import { MdDialog } from '@material/web/dialog/dialog'; import { DOMEvent } from '../../titanium/types/dom-event'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; @@ -19,20 +19,26 @@ import { dialogCloseNavigationHack, dialogOpenNavigationHack } from '../../titan import ApiService from '../api-service/api-service'; import { SnackbarStack } from '../../titanium/snackbar/snackbar-stack'; import { p } from '../../titanium/styles/p'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type CloseReason = 'done'; @customElement('leavitt-view-sent-email-dialog') -export class LeavittViewSentEmailDialog extends LoadWhile(LitElement) { - @property({ type: Object }) accessor apiService: ApiService | null; +export class LeavittViewSentEmailDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; - @state() private accessor emailTemplateLogId: number | null; - @state() private accessor emailTemplateLog: Partial | null; + @property({ type: Object }) accessor apiService: ApiService | null = null; + + @state() private accessor emailTemplateLogId: number | null = null; + @state() private accessor emailTemplateLog: Partial | null = null; @query('titanium-snackbar-stack') private accessor snackbar!: SnackbarStack; - @query('md-dialog') private accessor dialog: MdDialog; + @query('md-dialog') private accessor dialog!: MdDialog; - #resolve: (value: CloseReason) => void; + #resolve!: (value: CloseReason) => void; async open(emailTemplateLogId: number) { this.emailTemplateLogId = emailTemplateLogId; this.emailTemplateLog = null; @@ -59,11 +65,11 @@ export class LeavittViewSentEmailDialog extends LoadWhile(LitElement) { try { const get = this.apiService.getAsync>(`EmailTemplateLogs(${emailTemplateLogId})?${odataParts.join('&')}`); - this.loadWhile(get); + this.trackLoadingPromise(get); const result = await get; return result?.entity; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return null; } diff --git a/packages/web/leavitt/error-page/error-page.ts b/packages/web/leavitt/error-page/error-page.ts index c560f22c3..d9237ebf3 100644 --- a/packages/web/leavitt/error-page/error-page.ts +++ b/packages/web/leavitt/error-page/error-page.ts @@ -4,11 +4,11 @@ import { property, customElement, query } from 'lit/decorators.js'; import '../app/app-main-content-container'; import '../app/app-width-limiter'; -import { Container, OptionsColor, tsParticles } from '@tsparticles/engine'; -import { loadStarsPreset } from '@tsparticles/preset-stars'; import { ThemePreference, ThemePreferenceOption } from '../theme/theme-preference'; import themePreferenceEvent from '../theme/theme-preference-event'; +type Star = { x: number; y: number; r: number; baseOpacity: number; phase: number; twinkleSpeed: number; vx: number; vy: number }; + /** * A pre-styled error page * @@ -24,45 +24,140 @@ export class LeavittErrorPage extends ThemePreference(LitElement) { @property() accessor heading: string | TemplateResult<1> = 'Hmm...'; @property() accessor message: string | TemplateResult<1> = "It looks like that page doesn't exist."; - @query('div[particles]') private accessor particlesContainer: HTMLDivElement; + @query('canvas[particles]') private accessor canvas!: HTMLCanvasElement; + + #mdSysColorBackground!: string; + #mdSysColorOnBackground!: string; + #ctx: CanvasRenderingContext2D | null = null; + #stars: Star[] = []; + #rafId = 0; + #resizeObserver?: ResizeObserver; + #reduceMotion = false; - #mdSysColorBackground: string; - #mdSysColorOnBackground: string; - #particles: Container | undefined; + #onThemeChange = (themePreference: ThemePreferenceOption) => { + this.#setColors(themePreference); + this.style.backgroundColor = this.#mdSysColorBackground; + if (this.#reduceMotion) { + this.#draw(); + } + }; - async firstUpdated() { + firstUpdated() { this.#setColors(this.themePreference); - themePreferenceEvent.subscribe('theme-preference', 'change', (themePreference: ThemePreferenceOption) => { - this.#setColors(themePreference); + this.style.backgroundColor = this.#mdSysColorBackground; + themePreferenceEvent.subscribe('theme-preference', 'change', this.#onThemeChange); - if (this.#particles) { - this.#particles.options.background.color = this.#mdSysColorBackground as unknown as OptionsColor; - this.#particles.options.particles.color.value = this.#mdSysColorOnBackground; - this.#particles.refresh(); - } - }); - - await loadStarsPreset(tsParticles); - - if (this.particlesContainer) { - this.#particles = await tsParticles.load({ - element: this.particlesContainer, - options: { - preset: 'stars', - background: { - color: this.#mdSysColorBackground, - }, - particles: { - color: { - value: this.#mdSysColorOnBackground, - }, - }, - fullScreen: { - enable: false, - }, - }, + this.#reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false; + + if (!this.canvas) { + return; + } + this.#ctx = this.canvas.getContext('2d'); + + this.#resizeObserver = new ResizeObserver(() => this.#resize()); + this.#resizeObserver.observe(this.canvas); + this.#resize(); + + this.#startLoop(); + } + + connectedCallback() { + super.connectedCallback(); + this.#startLoop(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + cancelAnimationFrame(this.#rafId); + this.#rafId = 0; + this.#resizeObserver?.disconnect(); + // EventBus.unsubscribe types the callback as EventCallback, not the dispatched arg type. + themePreferenceEvent.unsubscribe('theme-preference', 'change', this.#onThemeChange as never); + } + + #startLoop() { + if (!this.#ctx || this.#reduceMotion || this.#rafId) { + return; + } + this.#rafId = requestAnimationFrame(this.#tick); + } + + #resize() { + if (!this.canvas) { + return; + } + const dpr = window.devicePixelRatio || 1; + const width = this.canvas.clientWidth; + const height = this.canvas.clientHeight; + if (width === 0 || height === 0) { + return; + } + this.canvas.width = Math.round(width * dpr); + this.canvas.height = Math.round(height * dpr); + this.#ctx?.setTransform(dpr, 0, 0, dpr, 0, 0); + this.#initStars(width, height); + if (this.#reduceMotion) { + this.#draw(); + } + } + + #initStars(width: number, height: number) { + const count = Math.min(160, Math.max(24, Math.round((width * height) / 9000))); + const stars: Star[] = []; + for (let i = 0; i < count; i++) { + stars.push({ + x: Math.random() * width, + y: Math.random() * height, + r: Math.random() * 1.6 + 0.7, + baseOpacity: Math.random() * 0.5 + 0.5, + phase: Math.random() * Math.PI * 2, + twinkleSpeed: Math.random() * 0.012 + 0.004, + vx: (Math.random() - 0.5) * 0.08, + vy: (Math.random() - 0.5) * 0.08, }); } + this.#stars = stars; + } + + #tick = () => { + if (!this.canvas) { + return; + } + const width = this.canvas.clientWidth; + const height = this.canvas.clientHeight; + for (const star of this.#stars) { + star.phase += star.twinkleSpeed; + star.x += star.vx; + star.y += star.vy; + if (star.x < 0) { + star.x += width; + } else if (star.x > width) { + star.x -= width; + } + if (star.y < 0) { + star.y += height; + } else if (star.y > height) { + star.y -= height; + } + } + this.#draw(); + this.#rafId = requestAnimationFrame(this.#tick); + }; + + #draw() { + if (!this.#ctx || !this.canvas) { + return; + } + this.#ctx.clearRect(0, 0, this.canvas.clientWidth, this.canvas.clientHeight); + this.#ctx.fillStyle = this.#mdSysColorOnBackground; + for (const star of this.#stars) { + const opacity = this.#reduceMotion ? star.baseOpacity : star.baseOpacity * (0.55 + 0.45 * Math.sin(star.phase)); + this.#ctx.globalAlpha = Math.max(0, Math.min(1, opacity)); + this.#ctx.beginPath(); + this.#ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2); + this.#ctx.fill(); + } + this.#ctx.globalAlpha = 1; } #setColors(themePreference: ThemePreferenceOption) { @@ -76,6 +171,7 @@ export class LeavittErrorPage extends ThemePreference(LitElement) { static styles = css` :host { display: grid; + position: relative; } h1 { @@ -141,9 +237,11 @@ export class LeavittErrorPage extends ThemePreference(LitElement) { } } - div[particles] { + canvas[particles] { position: absolute; inset: 0; + width: 100%; + height: 100%; } @keyframes spin { @@ -160,7 +258,7 @@ export class LeavittErrorPage extends ThemePreference(LitElement) { return html` -
+

${this.heading}

${this.message}

diff --git a/packages/web/leavitt/file-explorer/add-folder-modal.ts b/packages/web/leavitt/file-explorer/add-folder-modal.ts index ce2a7ee14..38ef3b281 100644 --- a/packages/web/leavitt/file-explorer/add-folder-modal.ts +++ b/packages/web/leavitt/file-explorer/add-folder-modal.ts @@ -11,22 +11,28 @@ import ApiService from '../api-service/api-service'; import { PendingStateEvent } from '../../titanium/types/pending-state-event'; import { DOMEvent } from '../../titanium/types/dom-event'; import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; -import { LoadWhile } from '../../titanium/helpers/helpers'; +import { promiseTracking } from '../../titanium/helpers/promise-tracking'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; @customElement('leavitt-add-folder-modal') -export class AddFolderModal extends LoadWhile(LitElement) { +export class AddFolderModal extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + /** * Required */ - @property({ attribute: false }) accessor apiService: ApiService | null; - @property({ type: Number }) accessor fileExplorerId: number; - @property({ type: Number }) accessor parentFolderId: number; + @property({ attribute: false }) accessor apiService: ApiService | null = null; + @property({ type: Number }) accessor fileExplorerId: number = 0; + @property({ type: Number }) accessor parentFolderId: number = 0; @state() private accessor folderName: string = ''; @query('md-dialog') private accessor dialog!: MdDialog; - resolve: (value: FileExplorerFolderDto | null) => void; + resolve!: (value: FileExplorerFolderDto | null) => void; async open() { this.folderName = ''; @@ -50,7 +56,7 @@ export class AddFolderModal extends LoadWhile(LitElement) { try { const post = this.apiService.postAsync('FileExplorerFolders?expand=CreatorPerson(select=FullName,ProfilePictureCdnFileName)', dto); this.dispatchEvent(new PendingStateEvent(post)); - this.loadWhile(post); + this.trackLoadingPromise(post); const result = (await post)?.entity; return { @@ -59,7 +65,7 @@ export class AddFolderModal extends LoadWhile(LitElement) { CreatorFullName: result?.CreatorPerson?.FullName, } as FileExplorerFolderDto; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return null; } diff --git a/packages/web/leavitt/file-explorer/file-explorer-image.ts b/packages/web/leavitt/file-explorer/file-explorer-image.ts index c50e4d66b..b90a200a6 100644 --- a/packages/web/leavitt/file-explorer/file-explorer-image.ts +++ b/packages/web/leavitt/file-explorer/file-explorer-image.ts @@ -11,7 +11,7 @@ import { Attachment } from '@leavittsoftware/lg-core-typescript/lg.net.core'; @customElement('leavitt-file-explorer-image') export class FileExplorerImage extends LitElement { - @property({ type: Object }) accessor attachment: Partial; + @property({ type: Object }) accessor attachment!: Partial; static styles = css` :host { diff --git a/packages/web/leavitt/file-explorer/file-explorer.ts b/packages/web/leavitt/file-explorer/file-explorer.ts index 358e9e9aa..f7cd300c4 100644 --- a/packages/web/leavitt/file-explorer/file-explorer.ts +++ b/packages/web/leavitt/file-explorer/file-explorer.ts @@ -1,4 +1,4 @@ -import { LoadWhile } from '../../titanium/helpers/load-while'; +import { promiseTracking } from '../../titanium/helpers/promise-tracking'; import { css, html, LitElement, nothing, PropertyValues } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import ApiService from '../api-service/api-service'; @@ -11,9 +11,8 @@ import { FileExplorerPathDto, } from '@leavittsoftware/lg-core-typescript'; import { PendingStateEvent } from '../../titanium/types/pending-state-event'; -import { ConfirmDialogOpenEvent } from '../../titanium/confirm-dialog/confirm-dialog-open-event'; -import '../../titanium/confirm-dialog/confirm-dialog'; +import '../../titanium/confirmation-dialog/confirmation-dialog'; import fileExplorerEvents from './events/file-explorer-events'; import '@material/web/icon/icon'; @@ -37,11 +36,12 @@ import { a, ellipsis, h1, h2 } from '../../titanium/styles/styles'; import { formatBytes } from './helpers/format-bytes'; import { CloseMenuEvent, MdMenu, MenuItem } from '@material/web/menu/menu'; -import TitaniumConfirmDialog from '../../titanium/confirm-dialog/confirm-dialog'; +import TitaniumConfirmationDialog from '../../titanium/confirmation-dialog/confirmation-dialog'; import { FileModal } from './file-modal'; import { AddFolderModal } from './add-folder-modal'; import { FolderModal } from './folder-modal'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; /** * Leavitt Group specific file explorer @@ -56,11 +56,16 @@ import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; * @fires file-deleted - Fired when a file is deleted. */ @customElement('leavitt-file-explorer') -export class LeavittFileExplorer extends LoadWhile(LitElement) { +export class LeavittFileExplorer extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + /** * This is required. */ - @property({ attribute: false }) accessor apiService: ApiService | null; + @property({ attribute: false }) accessor apiService: ApiService | null = null; /** * ID File explorer to display. This is required. @@ -101,14 +106,14 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { @state() private accessor path: FileExplorerPathDto[] = []; @state() private accessor selected: ((FileExplorerFolderDto | FileExplorerFileDto) & { type: 'folder' | 'file' })[] = []; - @query('md-menu[upload-menu]') private accessor uploadMenu: MdMenu; + @query('md-menu[upload-menu]') private accessor uploadMenu!: MdMenu; - @query('leavitt-folder-modal') private accessor folderDialog: FolderModal; - @query('leavitt-add-folder-modal') private accessor addFolderDialog: AddFolderModal; - @query('leavitt-file-modal') private accessor fileDialog: FileModal; - @query('input[files]') private accessor fileInput: HTMLInputElement; - @query('input[folders]') private accessor folderInput: HTMLInputElement; - @query('titanium-confirm-dialog') private accessor confirmDialog: TitaniumConfirmDialog; + @query('leavitt-folder-modal') private accessor folderDialog!: FolderModal; + @query('leavitt-add-folder-modal') private accessor addFolderDialog!: AddFolderModal; + @query('leavitt-file-modal') private accessor fileDialog!: FileModal; + @query('input[files]') private accessor fileInput!: HTMLInputElement; + @query('input[folders]') private accessor folderInput!: HTMLInputElement; + @query('titanium-confirmation-dialog') private accessor confirmationDialog!: TitaniumConfirmationDialog; #originalFolderId = 0; @@ -116,15 +121,10 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { //force attribute to reflect this.display = structuredClone(this.display); - this.addEventListener(ConfirmDialogOpenEvent.eventType, async (e: ConfirmDialogOpenEvent) => { + this.addEventListener(PendingStateEvent.eventType, (async (e: PendingStateEvent) => { e.stopPropagation(); - this.confirmDialog.handleEvent(e); - }); - - this.addEventListener(PendingStateEvent.eventType, async (e: PendingStateEvent) => { - e.stopPropagation(); - this.loadWhile(e.detail.promise); - }); + this.trackLoadingPromise(e.detail.promise); + }) as unknown as EventListener); fileExplorerEvents.subscribe('FileExplorerFileDto', 'Update', (o) => { const index = this.files.findIndex((file) => file.Id === o.Id); @@ -169,7 +169,7 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { try { const get = this.apiService?.getAsync(`FileExplorers(${fileExplorerId})/FileExplorerView(folderId=${folderId})`); if (get) { - this.loadWhile(get); + this.trackLoadingPromise(get); } const result = await get; @@ -197,7 +197,8 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { this.state = this.folders.length > 0 || this.files.length > 0 ? 'files' : 'no-files'; } } catch (error) { - if (error?.statusCode == 401 || error?.statusCode == 404) { + const httpError = error as Partial; + if (httpError?.statusCode == 401 || httpError?.statusCode == 404) { this.path = [{ Name: 'Files' } as FileExplorerPathDto]; this.state = 'no-permission'; return; @@ -224,12 +225,11 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { * @internal */ async #deleteSelectedClick() { - const confirmationDialogEvent = new ConfirmDialogOpenEvent( + const result = await this.confirmationDialog.open( 'Please confirm delete', `Deleting folders will delete all of their contents. Are you sure you would like to delete the selected item${this.selected.length === 1 ? '' : 's'}?` ); - this.dispatchEvent(confirmationDialogEvent); - if (await confirmationDialogEvent.dialogResult) { + if (result === 'confirmed') { const items = [...this.selected]; const errorMessageToCount: Map = new Map(); let totalErrorCount = 0; @@ -263,13 +263,14 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { this.dispatchEvent(new CustomEvent('file-deleted')); } } catch (newError) { - const newErrorCount = (errorMessageToCount.get(newError) ?? 0) + 1; - errorMessageToCount.set(newError, newErrorCount); + const message = newError instanceof Error ? newError.message : String(newError); + const newErrorCount = (errorMessageToCount.get(message) ?? 0) + 1; + errorMessageToCount.set(message, newErrorCount); totalErrorCount++; } }) ); - this.loadWhile(requests); + this.trackLoadingPromise(requests); await requests; this.selected = []; this.state = this.folders.length > 0 || this.files.length > 0 ? 'files' : 'no-files'; @@ -360,12 +361,13 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { } } } catch (error) { - failedFiles.push(file.name + ': ' + error.message); + const message = error instanceof Error ? error.message : String(error); + failedFiles.push(file.name + ': ' + message); } }); const uploadAll = Throttle.all(requests, { maxInProgress: 4 }); - this.loadWhile(uploadAll); + this.trackLoadingPromise(uploadAll); await uploadAll; if (failedFiles.length > 0) { @@ -403,12 +405,13 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { } } } catch (error) { - failedFiles.push(file.webkitRelativePath + ': ' + error.message); + const message = error instanceof Error ? error.message : String(error); + failedFiles.push(file.webkitRelativePath + ': ' + message); } }); const uploadAll = Throttle.all(requests, { maxInProgress: 4 }); - this.loadWhile(uploadAll); + this.trackLoadingPromise(uploadAll); await uploadAll; if (failedFiles.length > 0) { @@ -446,7 +449,7 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { } return result; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } return null; } @@ -864,7 +867,7 @@ export class LeavittFileExplorer extends LoadWhile(LitElement) { > - + `; } } diff --git a/packages/web/leavitt/file-explorer/file-list-item.ts b/packages/web/leavitt/file-explorer/file-list-item.ts index 0a342421b..980ec6d0f 100644 --- a/packages/web/leavitt/file-explorer/file-list-item.ts +++ b/packages/web/leavitt/file-explorer/file-list-item.ts @@ -14,7 +14,7 @@ import { formatBytes } from './helpers/format-bytes'; @customElement('file-list-item') export class FileListItem extends LitElement { - @property({ type: Object }) accessor file: FileExplorerFileDto; + @property({ type: Object }) accessor file!: FileExplorerFileDto; @property({ type: Boolean, reflect: true }) accessor selected: boolean = false; @property({ type: Number, reflect: true, attribute: 'selected-count' }) accessor selectedCount: number = 0; @property({ type: String, reflect: true, attribute: 'display' }) accessor display: 'grid' | 'list' = 'grid'; diff --git a/packages/web/leavitt/file-explorer/file-modal.ts b/packages/web/leavitt/file-explorer/file-modal.ts index 958ecafb9..43c007548 100644 --- a/packages/web/leavitt/file-explorer/file-modal.ts +++ b/packages/web/leavitt/file-explorer/file-modal.ts @@ -20,19 +20,25 @@ import { MdDialog } from '@material/web/dialog/dialog'; import { DOMEvent } from '../../titanium/types/dom-event'; import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; import ApiService from '../api-service/api-service'; -import { LoadWhile } from '../../titanium/helpers/helpers'; +import { promiseTracking } from '../../titanium/helpers/helpers'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; @customElement('leavitt-file-modal') -export class FileModal extends LoadWhile(LitElement) { - @property({ attribute: false }) accessor apiService: ApiService | null; +export class FileModal extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + + @property({ attribute: false }) accessor apiService: ApiService | null = null; @property({ type: Boolean }) accessor enableEditing: boolean = false; @state() private accessor state: 'view' | 'edit' = 'view'; @state() private accessor file: FileExplorerFileDto | null = null; @state() private accessor isCopying: boolean = false; @state() private accessor hasClipboard: boolean = false; - @state() private accessor fileName: string; + @state() private accessor fileName: string = ''; @query('md-dialog') private accessor dialog!: MdDialog; firstUpdated() { @@ -58,12 +64,12 @@ export class FileModal extends LoadWhile(LitElement) { try { const patch = this.apiService.patchAsync(`FileExplorerAttachments(${this.file?.Id})`, dto); - this.loadWhile(patch); + this.trackLoadingPromise(patch); await patch; fileExplorerEvents.dispatch('FileExplorerFileDto', 'Update', { ...this.file, Name: this.fileName }); this.state = 'view'; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } diff --git a/packages/web/leavitt/file-explorer/folder-list-item.ts b/packages/web/leavitt/file-explorer/folder-list-item.ts index f977daaf1..dc63456e5 100644 --- a/packages/web/leavitt/file-explorer/folder-list-item.ts +++ b/packages/web/leavitt/file-explorer/folder-list-item.ts @@ -10,7 +10,7 @@ import { FileExplorerFolderDto } from '@leavittsoftware/lg-core-typescript'; @customElement('folder-list-item') export class FolderListItem extends LitElement { - @property({ type: Object }) accessor folder: FileExplorerFolderDto; + @property({ type: Object }) accessor folder!: FileExplorerFolderDto; @property({ type: Boolean, reflect: true }) accessor selected: boolean = false; @property({ type: Number }) accessor selectedCount: number = 0; @property({ type: String, reflect: true, attribute: 'display' }) accessor display: 'grid' | 'list' = 'grid'; diff --git a/packages/web/leavitt/file-explorer/folder-modal.ts b/packages/web/leavitt/file-explorer/folder-modal.ts index 544a14a08..7e893015a 100644 --- a/packages/web/leavitt/file-explorer/folder-modal.ts +++ b/packages/web/leavitt/file-explorer/folder-modal.ts @@ -16,17 +16,23 @@ import { MdDialog } from '@material/web/dialog/dialog'; import { DOMEvent } from '../../titanium/types/dom-event'; import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; import ApiService from '../api-service/api-service'; -import { LoadWhile } from '../../titanium/helpers/helpers'; +import { promiseTracking } from '../../titanium/helpers/helpers'; import { ShowSnackbarEvent } from '../..//titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; @customElement('leavitt-folder-modal') -export class FolderModal extends LoadWhile(LitElement) { - @property({ attribute: false }) accessor apiService: ApiService | null; +export class FolderModal extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + + @property({ attribute: false }) accessor apiService: ApiService | null = null; @property({ type: Boolean }) accessor enableEditing: boolean = false; @state() private accessor state: 'view' | 'edit' = 'view'; @state() private accessor folder: FileExplorerFolderDto | null = null; - @state() private accessor folderName: string; + @state() private accessor folderName: string = ''; @query('md-dialog') private accessor dialog!: MdDialog; @@ -49,12 +55,12 @@ export class FolderModal extends LoadWhile(LitElement) { try { const patch = this.apiService.patchAsync(`FileExplorerFolders(${this.folder?.Id})`, dto); - this.loadWhile(patch); + this.trackLoadingPromise(patch); await patch; fileExplorerEvents.dispatch('FileExplorerFolder', 'Update', { ...this.folder, Name: this.folderName }); this.state = 'view'; } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } diff --git a/packages/web/leavitt/person-company-select/person-company-select.ts b/packages/web/leavitt/person-company-select/person-company-select.ts index 18f8277f1..ec6a2f8ef 100644 --- a/packages/web/leavitt/person-company-select/person-company-select.ts +++ b/packages/web/leavitt/person-company-select/person-company-select.ts @@ -11,6 +11,7 @@ import ApiService from '../api-service/api-service'; import Fuse from 'fuse.js'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; import { getCompanyMarkUrl } from '../../titanium/helpers/get-company-mark-url'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type Person = CorePerson & { type: 'Person'; Name: string }; export type Company = CoreCompany & { type: 'Company' }; @@ -42,7 +43,7 @@ export class LeavittPersonCompanySelect extends TitaniumSingleSelectBase (p.type = 'Person')); return results; } catch (error) { - if (error?.name !== 'AbortError' && !error?.message?.includes('Abort error')) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + const err = error as Partial & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } return null; @@ -134,8 +136,9 @@ export class LeavittPersonCompanySelect extends TitaniumSingleSelectBase (p.type = 'Company')); return results; } catch (error) { - if (error?.name !== 'AbortError' && !error?.message?.includes('Abort error')) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + const err = error as Partial & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } return null; diff --git a/packages/web/leavitt/person-group-select/person-group-select.ts b/packages/web/leavitt/person-group-select/person-group-select.ts index dd2779862..1b6e9afb9 100644 --- a/packages/web/leavitt/person-group-select/person-group-select.ts +++ b/packages/web/leavitt/person-group-select/person-group-select.ts @@ -11,6 +11,7 @@ import ApiService from '../api-service/api-service'; import Fuse, { IFuseOptions } from 'fuse.js'; import { peopleGroupIcons } from './people-group-icons'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type Person = CorePerson & { type: 'Person'; Name: string }; export type PeopleGroup = CorePeopleGroup & { type: 'PeopleGroup' }; @@ -46,7 +47,7 @@ export class LeavittPersonGroupSelect extends TitaniumSingleSelectBase this.#doSearch(searchTerm)); #abortController: AbortController = new AbortController(); @@ -68,7 +69,7 @@ export class LeavittPersonGroupSelect extends TitaniumSingleSelectBase (p.type = 'Person')); return results; } catch (error) { - if (error?.name !== 'AbortError' && !error?.message?.includes('Abort error')) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + const err = error as Partial & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } return null; @@ -138,8 +140,9 @@ export class LeavittPersonGroupSelect extends TitaniumSingleSelectBase (p.type = 'PeopleGroup')); return results; } catch (error) { - if (error?.name !== 'AbortError' && !error?.message?.includes('Abort error')) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + const err = error as Partial & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } return null; diff --git a/packages/web/leavitt/person-select/person-select.ts b/packages/web/leavitt/person-select/person-select.ts index a95192113..299e7791a 100644 --- a/packages/web/leavitt/person-select/person-select.ts +++ b/packages/web/leavitt/person-select/person-select.ts @@ -10,6 +10,7 @@ import '../profile-picture/profile-picture'; import { Debouncer, getSearchTokens } from '../../titanium/helpers/helpers'; import ApiService from '../api-service/api-service'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; /** * Single select input that searches Leavitt Group users @@ -33,7 +34,7 @@ export class LeavittPersonSelect extends TitaniumSingleSelectBase)); } return []; } @@ -141,13 +142,14 @@ export class LeavittPersonSelect extends TitaniumSingleSelectBase(`${this.apiControllerName}?${oDataParts.join('&')}`, { abortController: this.#abortController }); - this.loadWhile(get); + this.trackLoadingPromise(get); const result = await get; this.showSuggestions(result?.entities ?? [], result?.odataCount ?? 0); } catch (error) { - if (error?.name !== 'AbortError' && !error?.message?.includes('Abort error')) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + const err = error as Partial & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } } diff --git a/packages/web/leavitt/profile-picture/profile-picture-menu.ts b/packages/web/leavitt/profile-picture/profile-picture-menu.ts index 5c0fcccf8..5c3b10eae 100644 --- a/packages/web/leavitt/profile-picture/profile-picture-menu.ts +++ b/packages/web/leavitt/profile-picture/profile-picture-menu.ts @@ -20,14 +20,14 @@ import { AuthZeroLgUserManager } from '../user-manager/auth-zero-lg-user-manager */ @customElement('profile-picture-menu') export class ProfilePictureMenu extends LitElement { - @property({ type: Object }) accessor userManager: AuthZeroLgUserManager | null; + @property({ type: Object }) accessor userManager: AuthZeroLgUserManager | null = null; /** * Size in pixels of profile picture button */ @property({ type: Number }) accessor size: number = 40; - @property({ type: String }) accessor profilePictureFileName: string | null; + @property({ type: String }) accessor profilePictureFileName: string | null = null; /** * Person id of user @@ -49,7 +49,7 @@ export class ProfilePictureMenu extends LitElement { */ @property({ type: String }) accessor name: string = ''; - @query('md-menu') private accessor menu: MdMenu; + @query('md-menu') private accessor menu!: MdMenu; @property() positioning: 'absolute' | 'fixed' | 'document' | 'popover' = 'popover'; diff --git a/packages/web/leavitt/profile-picture/profile-picture.ts b/packages/web/leavitt/profile-picture/profile-picture.ts index 85ee1d11d..b1f7235f6 100644 --- a/packages/web/leavitt/profile-picture/profile-picture.ts +++ b/packages/web/leavitt/profile-picture/profile-picture.ts @@ -16,7 +16,7 @@ export class ProfilePicture extends LitElement { /** * File name of the profile picture on CDN, no extension */ - @property({ reflect: true, type: String }) accessor fileName: string | null; + @property({ reflect: true, type: String }) accessor fileName: string | null = null; /** * Shape of profile picture @@ -26,17 +26,17 @@ export class ProfilePicture extends LitElement { /** * Shows a colored ring around the picture */ - @property({ reflect: true, type: Boolean, attribute: 'show-ring' }) accessor showRing: boolean; + @property({ reflect: true, type: Boolean, attribute: 'show-ring' }) accessor showRing: boolean = false; /** * Shows a test user indicator at the bottom of the picture */ - @property({ reflect: true, type: Boolean, attribute: 'show-test-user-indicator' }) accessor showTestUserIndicator: boolean; + @property({ reflect: true, type: Boolean, attribute: 'show-test-user-indicator' }) accessor showTestUserIndicator: boolean = false; /** * Makes the image a link to the respective profile page */ - @property({ reflect: true, type: Number, attribute: 'profile-picture-link-person-id' }) accessor profilePictureLinkPersonId: number | null; + @property({ reflect: true, type: Number, attribute: 'profile-picture-link-person-id' }) accessor profilePictureLinkPersonId: number | null = null; /** * Size in pixels of profile picture diff --git a/packages/web/leavitt/service-worker-notifier/service-worker-notifier.ts b/packages/web/leavitt/service-worker-notifier/service-worker-notifier.ts index aca59d99b..f56b857b9 100644 --- a/packages/web/leavitt/service-worker-notifier/service-worker-notifier.ts +++ b/packages/web/leavitt/service-worker-notifier/service-worker-notifier.ts @@ -6,11 +6,11 @@ import '@material/web/progress/circular-progress'; @customElement('leavitt-service-worker-notifier') export class LeavittServiceWorkerNotifier extends ThemePreference(LitElement) { - @query('main') private accessor main: HTMLElement; + @query('main') private accessor main!: HTMLElement; @state() private accessor isLoading: boolean = false; - #newWorker: ServiceWorker | null; + #newWorker!: ServiceWorker | null; #refreshing = false; async connectedCallback() { diff --git a/packages/web/leavitt/theme/theme-preference.ts b/packages/web/leavitt/theme/theme-preference.ts index f1a81af74..1d27ba42b 100644 --- a/packages/web/leavitt/theme/theme-preference.ts +++ b/packages/web/leavitt/theme/theme-preference.ts @@ -19,7 +19,7 @@ export declare class ThemePreferenceInterface { */ export const ThemePreference = >(superClass: T) => { class ThemePreference extends superClass { - @property({ attribute: 'theme-preference', reflect: true }) themePreference: ThemePreferenceOption; + @property({ attribute: 'theme-preference', reflect: true }) themePreference: ThemePreferenceOption = 'light'; connectedCallback() { super.connectedCallback(); diff --git a/packages/web/leavitt/user-feedback/provide-feedback-dialog.ts b/packages/web/leavitt/user-feedback/provide-feedback-dialog.ts index c54c748b6..fc0e770f6 100644 --- a/packages/web/leavitt/user-feedback/provide-feedback-dialog.ts +++ b/packages/web/leavitt/user-feedback/provide-feedback-dialog.ts @@ -5,8 +5,8 @@ import '@material/web/button/text-button'; import '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; import { LitElement, css, html } from 'lit'; -import { customElement, property, query } from 'lit/decorators.js'; -import { LoadWhile, isDevelopment } from '../../titanium/helpers/helpers'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { promiseTracking, isDevelopment } from '../../titanium/helpers/helpers'; import { PendingStateEvent } from '../../titanium/types/pending-state-event'; import { h1, p } from '../../titanium/styles/styles'; import { IssueDto } from '@leavittsoftware/lg-core-typescript'; @@ -19,13 +19,19 @@ import { DOMEvent } from '../../titanium/types/dom-event'; import { SnackbarStack } from '../../titanium/snackbar/snackbar-stack'; import { dialogCloseNavigationHack, dialogOpenNavigationHack } from '../../titanium/hacks/dialog-navigation-hack'; import { AuthZeroLgUserManager } from '../user-manager/auth-zero-lg-user-manager'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; @customElement('provide-feedback-dialog') -export class ProvideFeedbackDialog extends LoadWhile(LitElement) { - @property({ type: Object }) accessor userManager: AuthZeroLgUserManager; +export class ProvideFeedbackDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + + @property({ type: Object }) accessor userManager!: AuthZeroLgUserManager; @query('md-dialog') private accessor dialog!: MdDialog; - @query('titanium-snackbar-stack') private accessor snackbar: SnackbarStack; - @query('md-outlined-text-field') private accessor textArea: MdOutlinedTextField; + @query('titanium-snackbar-stack') private accessor snackbar!: SnackbarStack; + @query('md-outlined-text-field') private accessor textArea!: MdOutlinedTextField; show() { this.reset(); @@ -57,7 +63,7 @@ export class ProvideFeedbackDialog extends LoadWhile(LitElement) { apiService.addHeader('X-LGAppName', 'IssueTracking'); const post = apiService.postAsync('Issues/ReportIssue', dto, { sendAsFormData: true }); this.dispatchEvent(new PendingStateEvent(post)); - this.loadWhile(post); + this.trackLoadingPromise(post); const entity = (await post).entity; if (!entity) { @@ -69,7 +75,7 @@ export class ProvideFeedbackDialog extends LoadWhile(LitElement) { } } } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } diff --git a/packages/web/leavitt/user-feedback/report-a-problem-dialog.ts b/packages/web/leavitt/user-feedback/report-a-problem-dialog.ts index 4c6d552e6..f1a2002d3 100644 --- a/packages/web/leavitt/user-feedback/report-a-problem-dialog.ts +++ b/packages/web/leavitt/user-feedback/report-a-problem-dialog.ts @@ -6,8 +6,8 @@ import '../../titanium/snackbar/snackbar-stack'; import '../../titanium/smart-attachment-input/smart-attachment-input'; import { LitElement, css, html } from 'lit'; -import { customElement, property, query } from 'lit/decorators.js'; -import { LoadWhile, isDevelopment } from '../../titanium/helpers/helpers'; +import { customElement, property, query, state } from 'lit/decorators.js'; +import { promiseTracking, isDevelopment } from '../../titanium/helpers/helpers'; import { PendingStateEvent } from '../../titanium/types/pending-state-event'; import { h1, p } from '../../titanium/styles/styles'; import { IssueDto } from '@leavittsoftware/lg-core-typescript'; @@ -21,15 +21,21 @@ import { DOMEvent } from '../../titanium/types/dom-event'; import { SnackbarStack } from '../../titanium/snackbar/snackbar-stack'; import { TitaniumSmartAttachmentInput } from '../../titanium/smart-attachment-input/smart-attachment-input'; import { AuthZeroLgUserManager } from '../user-manager/auth-zero-lg-user-manager'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; @customElement('report-a-problem-dialog') -export class ReportAProblemDialog extends LoadWhile(LitElement) { - @property({ type: Object }) accessor userManager: AuthZeroLgUserManager; +export class ReportAProblemDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + + @property({ type: Object }) accessor userManager!: AuthZeroLgUserManager; @query('md-dialog') private accessor dialog!: MdDialog; - @query('titanium-snackbar-stack') private accessor snackbar: SnackbarStack; + @query('titanium-snackbar-stack') private accessor snackbar!: SnackbarStack; - @query('md-outlined-text-field') private accessor textArea: MdOutlinedTextField; - @query('titanium-smart-attachment-input') private accessor imageInput: TitaniumSmartAttachmentInput | null; + @query('md-outlined-text-field') private accessor textArea!: MdOutlinedTextField; + @query('titanium-smart-attachment-input') private accessor imageInput!: TitaniumSmartAttachmentInput | null; show() { this.reset(); @@ -61,7 +67,7 @@ export class ReportAProblemDialog extends LoadWhile(LitElement) { apiService.addHeader('X-LGAppName', 'IssueTracking'); const post = apiService.postAsync('Issues/ReportIssue', dto, { sendAsFormData: true }); this.dispatchEvent(new PendingStateEvent(post)); - this.loadWhile(post); + this.trackLoadingPromise(post); const entity = (await post).entity; if (!entity) { @@ -78,7 +84,7 @@ export class ReportAProblemDialog extends LoadWhile(LitElement) { this.dialog.close('done'); } } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } diff --git a/packages/web/leavitt/user-feedback/user-feedback.ts b/packages/web/leavitt/user-feedback/user-feedback.ts deleted file mode 100644 index 744c32347..000000000 --- a/packages/web/leavitt/user-feedback/user-feedback.ts +++ /dev/null @@ -1,210 +0,0 @@ -import '../../titanium/header/header'; -import '../../titanium/card/card'; -import '../../titanium/smart-attachment-input/smart-attachment-input'; - -import '@material/web/button/filled-tonal-button'; -import '@material/web/textfield/outlined-text-field'; -import '@material/web/tabs/primary-tab'; -import '@material/web/tabs/tabs'; - -import { LitElement, PropertyValues, css, html } from 'lit'; -import { customElement, property, query, state } from 'lit/decorators.js'; -import { LoadWhile, isDevelopment } from '../../titanium/helpers/helpers'; -import { PendingStateEvent } from '../../titanium/types/pending-state-event'; -import { h1, p } from '../../titanium/styles/styles'; -import { IssueDto } from '@leavittsoftware/lg-core-typescript'; -import { TitaniumSmartAttachmentInput } from '../../titanium/smart-attachment-input/smart-attachment-input'; -import ApiService from '../api-service//api-service'; -import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; -import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; -import { AuthZeroLgUserManager } from '../user-manager/auth-zero-lg-user-manager'; - -@customElement('leavitt-user-feedback') -export class LeavittUserFeedback extends LoadWhile(LitElement) { - @property({ type: Object }) accessor userManager: AuthZeroLgUserManager; - @property({ type: Boolean }) accessor isActive: boolean = false; - - @state() private accessor activeIndex: number = 0; - - @query('md-outlined-text-field') private accessor textArea: MdOutlinedTextField; - @query('titanium-smart-attachment-input') private accessor imageInput: TitaniumSmartAttachmentInput | null; - - async updated(changedProps: PropertyValues) { - if (changedProps.has('isActive') && this.isActive) { - this.reset(); - } - } - - reset() { - this.imageInput?.reset(); - this.textArea?.reset(); - } - - async #submitProblem() { - if (!this.textArea.reportValidity() || this.isLoading) { - return; - } - - const dto: IssueDto = { - SiteName: location.hostname, - PathName: window.location.pathname + window.location.search, - IssueType: 'Bug', - Description: this.textArea.value, - Attachments: (this.imageInput?.getFiles() ?? []).map((o) => o.file), - }; - - try { - const apiService = new ApiService(this.userManager); - apiService.baseUrl = isDevelopment ? 'https://devapi3.leavitt.com/' : 'https://api3.leavitt.com/'; - apiService.addHeader('X-LGAppName', 'IssueTracking'); - const post = apiService.postAsync('Issues/ReportIssue', dto, { sendAsFormData: true }); - this.dispatchEvent(new PendingStateEvent(post)); - this.loadWhile(post); - const entity = (await post).entity; - - if (!entity) { - throw new Error('Error submitting problem. Please try again.'); - } else { - this.dispatchEvent( - new ShowSnackbarEvent('', { - overrideTemplate: html`Thank you for bringing this issue to our attention!
-
- Our engineering teams will promptly investigate and address it.`, - }) - ); - this.reset(); - } - } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); - } - } - - async #submitFeedback() { - if (!this.textArea.reportValidity() || this.isLoading) { - return; - } - - const dto: IssueDto = { - SiteName: location.hostname, - PathName: window.location.pathname + window.location.search, - IssueType: 'Feedback', - Description: this.textArea.value, - Attachments: [], - }; - - try { - const apiService = new ApiService(this.userManager); - apiService.baseUrl = isDevelopment ? 'https://devapi3.leavitt.com/' : 'https://api3.leavitt.com/'; - apiService.addHeader('X-LGAppName', 'IssueTracking'); - const post = apiService.postAsync('Issues/ReportIssue', dto, { sendAsFormData: true }); - this.dispatchEvent(new PendingStateEvent(post)); - this.loadWhile(post); - const entity = (await post).entity; - - if (!entity) { - throw new Error('Error submitting feedback. Please try again.'); - } else { - this.dispatchEvent(new ShowSnackbarEvent('We appreciate your input, and we will promptly conduct a review!')); - this.reset(); - } - } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); - } - } - - static styles = [ - h1, - p, - css` - :host { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 24px; - } - - form { - display: flex; - flex-direction: column; - gap: 24px; - margin: 24px 0; - } - - titanium-card { - padding-top: 0; - } - - md-tabs { - margin-bottom: 16px; - --md-primary-tab-container-shape: 12px; - } - - md-outlined-text-field { - resize: none; - } - - [hidden] { - display: none !important; - } - `, - ]; - - render() { - return html` - - - - { - this.reset(); - this.activeIndex = event.target.activeTabIndex; - }} - > - Report a problem - person_alert - - Provide feedback - rate_review - - - -
- ${this.activeIndex === 0 - ? html` -

- Please be specific and provide screenshots of the issue if possible in your report. Your report goes directly to our engineering teams so it - can be addressed as soon as possible. -

- - - ` - : html`
-

- User feedback is a valuable tool that empowers our users to share their thoughts, suggestions, and concerns, helping us improve the overall - user experience of our websites and tools. We welcome and appreciate user feedback as it enables us to make informed decisions and enhance our - website based on the needs and expectations of our users. -

-

- Please be specific and provide as much detail as possible in your feedback. Your feedback goes directly to our development teams so it can be - carefully reviewed and planned into the next development cycle. -

- -
`} -
- - (this.activeIndex === 0 ? this.#submitProblem() : this.#submitFeedback())} ?disabled=${this.isLoading} - >Submit - -
- `; - } -} diff --git a/packages/web/leavitt/user-manager/auth-zero-lg-user-manager.ts b/packages/web/leavitt/user-manager/auth-zero-lg-user-manager.ts index 52643c662..80827a2e3 100644 --- a/packages/web/leavitt/user-manager/auth-zero-lg-user-manager.ts +++ b/packages/web/leavitt/user-manager/auth-zero-lg-user-manager.ts @@ -28,7 +28,7 @@ export class AuthZeroLgUserManager implements BearerTokenProvider { #isInitialized: boolean = false; #unrecoverableError: boolean = false; - #unrecoverableErrorDescription: string; + #unrecoverableErrorDescription!: string; public get identity() { return this.#decodeIdentity(this.#idToken); @@ -116,7 +116,7 @@ export class AuthZeroLgUserManager implements BearerTokenProvider { try { await this.#exchangeCodeForToken(code); } catch (error) { - return this.#rejectAllAuthenticatePromises(error.message); + return this.#rejectAllAuthenticatePromises(error instanceof Error ? error.message : String(error)); } if (this.#validateToken(this.#accessToken)) { @@ -171,7 +171,7 @@ export class AuthZeroLgUserManager implements BearerTokenProvider { //try to get a new token await this.#refreshAccessToken(this.#refreshToken); } catch (error) { - return this.#rejectAllAuthenticatePromises(error.message); + return this.#rejectAllAuthenticatePromises(error instanceof Error ? error.message : String(error)); } if (this.#validateToken(this.#accessToken)) { @@ -369,7 +369,7 @@ export class AuthZeroLgUserManager implements BearerTokenProvider { return data.access_token; } catch (error) { if (!navigator.onLine) { - throw new Error('No internet connection. Please check your network.'); + throw new Error('No internet connection. Please check your network.', { cause: error }); } console.error('Error refreshing access token', error); } @@ -414,10 +414,10 @@ export class AuthZeroLgUserManager implements BearerTokenProvider { return data.access_token; } catch (error) { if (!navigator.onLine) { - throw new Error('No internet connection. Please check your network.'); + throw new Error('No internet connection. Please check your network.', { cause: error }); } console.error('Token exchange failed', error); - throw new Error('Token exchange failed'); + throw new Error('Token exchange failed', { cause: error }); } } diff --git a/packages/web/package.json b/packages/web/package.json index c8fb77e8d..91c26782b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -4,6 +4,7 @@ "license": "Apache-2.0", "description": "", "files": [ + "CLAUDE.md", "**/*.js", "**/*.js.map", "**/*.d.ts", @@ -15,19 +16,16 @@ "**/*.jpg" ], "dependencies": { - "@googlemaps/js-api-loader": "1.16.10", + "@googlemaps/js-api-loader": "^2.1.1", "@leavittsoftware/lg-core-typescript": "*", "@lit/context": "^1.1.6", "@material/web": "*", - "@tsparticles/engine": "^3.9.1", - "@tsparticles/preset-stars": "^3.2.0", "@types/google.maps": "*", - "bowser": "^2.12.1", - "countup.js": "^2.9.0", - "cropperjs": "2.1.0", - "dayjs": "^1.11.19", + "bowser": "^2.14.1", + "cropperjs": "2.1.1", + "dayjs": "^1.11.21", "export-to-csv": "^1.4.0", - "fuse.js": "^7.1.0", + "fuse.js": "^7.4.2", "jwt-decode": "^4.0.0", "promise-parallel-throttle": "^3.5.0", "regexparam": "~3.0.0", diff --git a/packages/web/titanium/access-denied-page/access-denied-page.ts b/packages/web/titanium/access-denied-page/access-denied-page.ts deleted file mode 100644 index 45f39be5b..000000000 --- a/packages/web/titanium/access-denied-page/access-denied-page.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { css, html, LitElement } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; - -/** - * A pre-styled access denied page - * - * @element titanium-access-denied-page - * - */ -@customElement('titanium-access-denied-page') -export class TitaniumAccessDeniedPage extends LitElement { - /** - * Reason text for the denial of access - */ - @property({ type: String }) accessor message: string = 'You do not have permission to access this application.'; - - static styles = css` - :host { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - - font-family: Roboto, sans-serif; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - - max-width: 1300px; - } - - header { - flex: 1 1 auto; - margin-right: 24px; - } - - h1 { - font-family: Metropolis, 'Roboto', 'Noto', sans-serif; - font-weight: 600; - font-size: 68px; - line-height: 75px; - margin: 0; - } - - h2 { - font-weight: 400; - margin: 8px 0 0 4px; - max-width: 75%; - } - - p { - font-size: 16px; - margin-top: 8px; - } - - svg { - flex-shrink: 0; - height: 280px; - width: 280px; - } - - @media (max-width: 768px) { - :host { - margin-top: 24px; - } - - h2 { - max-width: inherit; - font-size: 21px; - } - - svg { - height: 120px; - width: 120px; - align-self: flex-start; - } - - h1 { - font-size: 55px; - line-height: 65px; - } - } - `; - - render() { - return html` -
-

Access denied!

-

${this.message}

-
- - - - - - - - - authentication - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `; - } -} diff --git a/packages/web/titanium/address-input/address-input.ts b/packages/web/titanium/address-input/address-input.ts index 303ca8b59..62a909edc 100644 --- a/packages/web/titanium/address-input/address-input.ts +++ b/packages/web/titanium/address-input/address-input.ts @@ -29,7 +29,7 @@ export class TitaniumAddressInput extends GoogleAddressInput { */ @property({ type: Boolean, attribute: 'show-county' }) accessor showCounty: boolean = false; - @query('manual-address-dialog') private accessor manualAddressDialog: ManualAddressDialog; + @query('manual-address-dialog') private accessor manualAddressDialog!: ManualAddressDialog; @property({ type: Boolean, attribute: 'has-selection', reflect: true }) private accessor hasSelection: boolean = false; @@ -44,7 +44,6 @@ export class TitaniumAddressInput extends GoogleAddressInput { } :host([has-selection]) { - --md-outlined-text-field-with-trailing-icon-trailing-space: 36px; --md-filled-text-field-with-trailing-icon-trailing-space: 36px; } `, @@ -63,7 +62,6 @@ export class TitaniumAddressInput extends GoogleAddressInput { .allowInternational=${this.allowInternational} .showCounty=${this.showCounty} .showStreet2=${this.showStreet2} - .filled=${this.filled} .label=${this.label} >`; } diff --git a/packages/web/titanium/address-input/google-address-input.ts b/packages/web/titanium/address-input/google-address-input.ts index ca13327e0..3e816260d 100644 --- a/packages/web/titanium/address-input/google-address-input.ts +++ b/packages/web/titanium/address-input/google-address-input.ts @@ -1,3 +1,6 @@ +/// +// Must precede the @googlemaps/js-api-loader import below; it reads process.env.NODE_ENV at module scope. +import './google-maps-process-shim.js'; import { PropertyValues, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -5,13 +8,14 @@ import { TitaniumSingleSelectBase } from '../../titanium/single-select-base/sing import { Debouncer } from '../../titanium/helpers/helpers'; import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; -import { Loader } from '@googlemaps/js-api-loader'; +import { setOptions, importLibrary } from '@googlemaps/js-api-loader'; import '@material/web/icon/icon'; -import { placeResultToAddress } from './utils/place-result-to-address'; +import { placeToAddress } from './utils/place-result-to-address'; import { addressToString } from './utils/address-to-string'; import { AddressInputAddress } from './types/address-input-address'; import { validateStreet } from './utils/validate-street'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; /** * Single select input that searches Places using the Google Places API @@ -24,7 +28,7 @@ export class GoogleAddressInput extends TitaniumSingleSelectBase this.#doSearch(searchTerm)); #abortController: AbortController = new AbortController(); - #placesService: google.maps.places.PlacesService; - #autoCompleteService: google.maps.places.AutocompleteService; + #sessionToken: google.maps.places.AutocompleteSessionToken | undefined; + #placePredictions = new Map(); async firstUpdated() { - const loader = new Loader({ - apiKey: this.googleMapsApiKey, - version: 'weekly', - libraries: ['places'], + setOptions({ + key: this.googleMapsApiKey, + v: 'weekly', }); - await loader.importLibrary('places'); + await importLibrary('places'); + this.#renewSessionToken(); + } - this.#placesService = new google.maps.places.PlacesService(document.createElement('div')); - this.#autoCompleteService = new google.maps.places.AutocompleteService(); + #renewSessionToken() { + this.#placePredictions.clear(); + this.#sessionToken = new google.maps.places.AutocompleteSessionToken(); } updated(changedProps: PropertyValues) { @@ -70,52 +76,59 @@ export class GoogleAddressInput extends TitaniumSingleSelectBase((res) => { - const autoCompletionRequest: google.maps.places.AutocompletionRequest = { - input: searchTerm, - types: ['address'], - }; - if (!this.allowInternational) { - autoCompletionRequest.componentRestrictions = { country: ['us'] }; - } - this.#autoCompleteService.getPlacePredictions( - autoCompletionRequest, - (predictions: google.maps.places.AutocompletePrediction[] | null, status: google.maps.places.PlacesServiceStatus) => { - if (status != google.maps.places.PlacesServiceStatus.OK || !predictions) { - console.warn(status); - return res([]); - } - - return res( - predictions.map( - (o) => - ({ - Id: o.place_id, - primaryDisplayText: o?.structured_formatting?.main_text || o.description, - secondaryText: o.structured_formatting?.secondary_text, - }) satisfies AddressInputAddress - ) - ); + const request: google.maps.places.AutocompleteRequest = { + input: searchTerm, + sessionToken: this.#sessionToken, + includedPrimaryTypes: ['street_address'], + }; + if (!this.allowInternational) { + request.includedRegionCodes = ['us']; + } + + try { + const { suggestions } = await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(request); + + this.#placePredictions.clear(); + const results: AddressInputAddress[] = []; + + for (const suggestion of suggestions) { + const prediction = suggestion.placePrediction; + if (!prediction) { + continue; } - ); - }); + + this.#placePredictions.set(prediction.placeId, prediction); + results.push({ + Id: prediction.placeId, + primaryDisplayText: prediction.mainText?.text ?? prediction.text.text, + secondaryText: prediction.secondaryText?.text, + }); + } + + return results; + } catch (error) { + throw new Error(this.#getPlacesApiErrorMessage(error), { cause: error }); + } + } + + #getPlacesApiErrorMessage(error: unknown) { + const message = String((error as Error)?.message ?? error); + if (message.includes('SERVICE_DISABLED')) { + return 'Google Places API (New) is not enabled for this API key. In Google Cloud Console, enable "Places API (New)" and "Maps JavaScript API" for the project that owns the key.'; + } + return message; } async #getPlaceDetail(placeId: string) { - return new Promise((res, rej) => { - const request: google.maps.places.PlaceDetailsRequest = { - placeId: placeId, - fields: ['address_components', 'formatted_address', 'geometry'], - }; - - this.#placesService.getDetails(request, (place, status) => { - if (status != google.maps.places.PlacesServiceStatus.OK || !place) { - return rej(status); - } + const prediction = this.#placePredictions.get(placeId); + const place = prediction?.toPlace() ?? new google.maps.places.Place({ id: placeId }); - return res(place); - }); + const { place: fetchedPlace } = await place.fetchFields({ + fields: ['addressComponents', 'formattedAddress', 'location'], }); + + this.#renewSessionToken(); + return fetchedPlace; } async #doSearch(searchTerm: string) { @@ -126,13 +139,14 @@ export class GoogleAddressInput extends TitaniumSingleSelectBase & { name?: string; message?: string }; + if (err?.name !== 'AbortError' && !err?.message?.includes('Abort error')) { + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } } @@ -198,12 +212,12 @@ export class GoogleAddressInput extends TitaniumSingleSelectBase)); } } super.setSelected(entity); diff --git a/packages/web/titanium/address-input/google-maps-process-shim.ts b/packages/web/titanium/address-input/google-maps-process-shim.ts new file mode 100644 index 000000000..baa6cfecf --- /dev/null +++ b/packages/web/titanium/address-input/google-maps-process-shim.ts @@ -0,0 +1,13 @@ +// @googlemaps/js-api-loader v2 reads process.env.NODE_ENV at module scope, which throws in +// browser environments that don't define `process` (raw ESM, esbuild w/o define, etc.). +// Define a minimal shim only when absent so the component is self-contained for consumers. +const globalProcess = globalThis as typeof globalThis & { + process?: { env?: Record }; +}; +if (typeof globalProcess.process === 'undefined') { + globalProcess.process = { env: { NODE_ENV: 'production' } }; +} else if (typeof globalProcess.process.env === 'undefined') { + globalProcess.process.env = { NODE_ENV: 'production' }; +} + +export {}; diff --git a/packages/web/titanium/address-input/manual-address-dialog.ts b/packages/web/titanium/address-input/manual-address-dialog.ts index 415e5304e..690f3ddcf 100644 --- a/packages/web/titanium/address-input/manual-address-dialog.ts +++ b/packages/web/titanium/address-input/manual-address-dialog.ts @@ -1,20 +1,15 @@ import '@material/web/icon/icon'; -import '@material/web/select/outlined-select'; import '@material/web/select/filled-select'; import '@material/web/select/select-option'; import '@material/web/dialog/dialog'; -import '@material/web/textfield/outlined-text-field'; import '@material/web/textfield/filled-text-field'; import '@material/web/button/filled-tonal-button'; import '@material/web/button/text-button'; -import { css, LitElement, nothing } from 'lit'; -import { literal, html } from 'lit/static-html.js'; +import { css, html, LitElement, nothing } from 'lit'; import { property, customElement, query, queryAll, state } from 'lit/decorators.js'; import { AddressInputAddress } from './types/address-input-address'; import { MdDialog } from '@material/web/dialog/dialog'; -import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; -import { MdOutlinedSelect } from '@material/web/select/outlined-select'; import { MdFilledTextField } from '@material/web/textfield/filled-text-field'; import { MdFilledSelect } from '@material/web/select/filled-select'; import { DOMEvent } from '../types/dom-event'; @@ -26,14 +21,13 @@ import { countries } from '../helpers/address/country-abbr-to-titlecase'; @customElement('manual-address-dialog') export class ManualAddressDialog extends LitElement { - @query('md-dialog') protected accessor dialog: MdDialog; + @query('md-dialog') protected accessor dialog!: MdDialog; @property({ type: String }) accessor label: string = ''; - @property({ type: Boolean, attribute: 'show-county' }) accessor showCounty: boolean; - @property({ type: Boolean, attribute: 'show-street2' }) accessor showStreet2: boolean; + @property({ type: Boolean, attribute: 'show-county' }) accessor showCounty: boolean = false; + @property({ type: Boolean, attribute: 'show-street2' }) accessor showStreet2: boolean = false; @property({ type: Boolean, attribute: 'allow-international' }) accessor allowInternational: boolean = false; - @property({ type: Boolean, attribute: 'filled' }) accessor filled: boolean = false; @state() protected accessor street: string = ''; @state() protected accessor street2: string = ''; @@ -43,11 +37,9 @@ export class ManualAddressDialog extends LitElement { @state() protected accessor state: string = ''; @state() protected accessor zip: string = ''; - @queryAll('md-outlined-text-field, md-outlined-select, md-filled-text-field, md-filled-select') protected accessor allInputs: NodeListOf< - MdOutlinedTextField | MdOutlinedSelect | MdFilledTextField | MdFilledSelect - >; + @queryAll('md-filled-text-field, md-filled-select') protected accessor allInputs!: NodeListOf; - resolve: (value: Partial | null) => void; + resolve!: (value: Partial | null) => void; public async open(address: AddressInputAddress | null | undefined) { this.reset(); @@ -130,7 +122,6 @@ export class ManualAddressDialog extends LitElement { ]; render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html` ) => { @@ -144,72 +135,65 @@ export class ManualAddressDialog extends LitElement { >
${this.label}
- <${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} + ) => reportValidityIfError(e.target)} - @change=${(e: DOMEvent) => (this.street = e.target.value)} + @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + @change=${(e: DOMEvent) => (this.street = e.target.value)} > markunread_mailbox - - ${ - this.showStreet2 || (this.country !== 'US' && this.country) - ? html` <${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} - @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + + ${this.showStreet2 || (this.country !== 'US' && this.country) + ? html`) => reportValidityIfError(e.target)} label="Street 2/Apartment" autocomplete="address-line2" .value=${this.street2 || ''} - @change=${(e: DOMEvent) => (this.street2 = e.target.value)} + @change=${(e: DOMEvent) => (this.street2 = e.target.value)} > - meeting_room` - : nothing - } - <${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} + meeting_room + ` + : nothing} + ) => reportValidityIfError(e.target)} - @change=${(e: DOMEvent) => (this.city = e.target.value)} - >location_city) => reportValidityIfError(e.target)} + @change=${(e: DOMEvent) => (this.city = e.target.value)} + >location_city - ${ - this.showCounty || (this.country !== 'US' && this.country) - ? html`<${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} - @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + ${this.showCounty || (this.country !== 'US' && this.country) + ? html`) => reportValidityIfError(e.target)} label="County" ?required=${!this.allowInternational || this.country === 'US'} .value=${this.county || ''} - @change=${(e: DOMEvent) => (this.county = e.target.value)} - >explore) => (this.county = e.target.value)} + >explore` - : nothing - } - ${ - this.allowInternational - ? html`<${this.filled ? literal`md-filled-select` : literal`md-outlined-select`} + : nothing} + ${this.allowInternational + ? html` preventDialogOverflow(this.dialog)} @closing=${() => allowDialogOverflow(this.dialog)} - @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} label="Country" autocomplete="country" required .value=${this.country || ''} - @change=${(e: DOMEvent) => { + @change=${(e: DOMEvent) => { e.stopPropagation(); this.country = e.target.value; if (this.country === 'US') { - // If manually typed state is a valid US state abbreviation or name, preselect it const foundState = usStates?.find( (s) => s.abbreviation.toLowerCase() === this.state.toLowerCase() || s.name?.toLowerCase() === this.state.toLowerCase() ); this.state = foundState ? foundState?.abbreviation : ''; } else if (this.country === 'CA') { - // If manually typed state is a valid CA state abbreviation or name, preselect it const foundState = caStates?.find( (s) => s.abbreviation.toLowerCase() === this.state.toLowerCase() || s.name?.toLowerCase() === this.state.toLowerCase() ); @@ -221,32 +205,30 @@ export class ManualAddressDialog extends LitElement { > map ${countries.map((s) => html`
${s.name}
`)} - ` - : nothing - } - ${ - this.allowInternational && this.country !== 'US' && this.country !== 'CA' - ? html` - <${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} +
` + : nothing} + ${this.allowInternational && this.country !== 'US' && this.country !== 'CA' + ? html` + ) => reportValidityIfError(e.target)} - @change=${(e: DOMEvent) => (this.state = e.target.value)} + @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + @change=${(e: DOMEvent) => (this.state = e.target.value)} > location_on - + ` - : html` - <${this.filled ? literal`md-filled-select` : literal`md-outlined-select`} + : html` + preventDialogOverflow(this.dialog)} @closing=${() => allowDialogOverflow(this.dialog)} - @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} + @blur=${(e: DOMEvent) => reportValidityIfError(e.target)} label="State" autocomplete="address-level1" required .value=${this.state || ''} - @change=${(e: DOMEvent) => { + @change=${(e: DOMEvent) => { e.stopPropagation(); this.state = e.target.value; if (usStates.some((o) => o.abbreviation.toLowerCase() === this.state.toLowerCase())) { @@ -274,18 +256,17 @@ export class ManualAddressDialog extends LitElement {
Canada
` )} - - ` - } +
+ `} - <${this.filled ? literal`md-filled-text-field` : literal`md-outlined-text-field`} + ) => reportValidityIfError(e.target)} - @change=${(e: DOMEvent) => (this.zip = e.target.value)} - >universal_local) => reportValidityIfError(e.target)} + @change=${(e: DOMEvent) => (this.zip = e.target.value)} + >universal_local @@ -312,6 +293,5 @@ export class ManualAddressDialog extends LitElement {
`; - /* eslint-enable lit/binding-positions, lit/no-invalid-html */ } } diff --git a/packages/web/titanium/address-input/utils/place-result-to-address.ts b/packages/web/titanium/address-input/utils/place-result-to-address.ts index 0b377358d..9cb060c0a 100644 --- a/packages/web/titanium/address-input/utils/place-result-to-address.ts +++ b/packages/web/titanium/address-input/utils/place-result-to-address.ts @@ -1,26 +1,29 @@ +/// import { AddressInputAddress } from '../types/address-input-address'; -export function placeResultToAddress(place: google.maps.places.PlaceResult) { - if (!place || !place.address_components || !place.formatted_address) { +export function placeToAddress(place: google.maps.places.Place) { + const addressComponents = place.addressComponents; + const formattedAddress = place.formattedAddress; + + if (!addressComponents?.length || !formattedAddress) { return null; } - const neighborhood = place.address_components.find((o) => o.types.some((p) => p === 'neighborhood')); - const stateComponent = place.address_components.find((o) => o.types.some((p) => p === 'administrative_area_level_1')); + const neighborhood = addressComponents.find((o) => o.types.some((p) => p === 'neighborhood')); + const stateComponent = addressComponents.find((o) => o.types.some((p) => p === 'administrative_area_level_1')); const streetNumberComponent = - place.address_components.find((o) => o.types.some((p) => p === 'street_number')) || - place.address_components.find((o) => o.types.some((p) => p === 'premise')); - const streetAddressComponent = place.address_components.find((o) => o.types.some((p) => p === 'route')); + addressComponents.find((o) => o.types.some((p) => p === 'street_number')) || addressComponents.find((o) => o.types.some((p) => p === 'premise')); + const streetAddressComponent = addressComponents.find((o) => o.types.some((p) => p === 'route')); - // GOOGLE flip-flops neighborhood and locality, neither can be used for a accurate city. formatted_address however seems to + // GOOGLE flip-flops neighborhood and locality, neither can be used for a accurate city. formattedAddress however seems to // always show the accurate city. - const locality = place.address_components.find((o) => o.types.some((p) => p === 'locality')); - const sublocality = place.address_components.find((o) => o.types.some((p) => p === 'sublocality')); - const adminAreaLevel3 = place.address_components.find((o) => o.types.some((p) => p === 'administrative_area_level_3')); - const adminAreaLevel4 = place.address_components.find((o) => o.types.some((p) => p === 'administrative_area_level_4')); + const locality = addressComponents.find((o) => o.types.some((p) => p === 'locality')); + const sublocality = addressComponents.find((o) => o.types.some((p) => p === 'sublocality')); + const adminAreaLevel3 = addressComponents.find((o) => o.types.some((p) => p === 'administrative_area_level_3')); + const adminAreaLevel4 = addressComponents.find((o) => o.types.some((p) => p === 'administrative_area_level_4')); const cityComponent = - neighborhood?.short_name && place.formatted_address.includes(neighborhood.short_name + ',') + neighborhood?.shortText && formattedAddress.includes(neighborhood.shortText + ',') ? neighborhood : locality ? locality @@ -29,24 +32,24 @@ export function placeResultToAddress(place: google.maps.places.PlaceResult) { : adminAreaLevel3 ? adminAreaLevel3 : adminAreaLevel4; - const zipCodeComponent = place.address_components.find((o) => o.types.some((p) => p === 'postal_code')); - const countyComponent = place.address_components.find((o) => o.types.some((p) => p === 'administrative_area_level_2')); - const countryComponent = place.address_components.find((o) => o.types.some((p) => p === 'country')); - const street2 = place.address_components.find((o) => o.types.some((p) => p === 'subpremise')); + const zipCodeComponent = addressComponents.find((o) => o.types.some((p) => p === 'postal_code')); + const countyComponent = addressComponents.find((o) => o.types.some((p) => p === 'administrative_area_level_2')); + const countryComponent = addressComponents.find((o) => o.types.some((p) => p === 'country')); + const street2 = addressComponents.find((o) => o.types.some((p) => p === 'subpremise')); const location: Partial = { - street: streetNumberComponent?.short_name - ? `${streetNumberComponent?.short_name} ${streetAddressComponent?.short_name || ''}`?.trim() - : streetAddressComponent?.short_name || '', - fullStreet: `${streetNumberComponent?.long_name} ${streetAddressComponent?.long_name}`, - city: cityComponent?.short_name, - street2: street2?.short_name, - county: countyComponent?.short_name, - country: countryComponent?.short_name, - state: stateComponent?.short_name, - fullState: stateComponent?.long_name, - zip: zipCodeComponent?.short_name, - latitude: place.geometry?.location?.lat(), - longitude: place.geometry?.location?.lng(), + street: streetNumberComponent?.shortText + ? `${streetNumberComponent?.shortText} ${streetAddressComponent?.shortText || ''}`?.trim() + : streetAddressComponent?.shortText || '', + fullStreet: `${streetNumberComponent?.longText} ${streetAddressComponent?.longText}`, + city: cityComponent?.shortText ?? undefined, + street2: street2?.shortText ?? undefined, + county: countyComponent?.shortText ?? undefined, + country: countryComponent?.shortText ?? undefined, + state: stateComponent?.shortText ?? undefined, + fullState: stateComponent?.longText ?? undefined, + zip: zipCodeComponent?.shortText ?? undefined, + latitude: place.location?.lat(), + longitude: place.location?.lng(), }; return location; diff --git a/packages/web/titanium/card/card.ts b/packages/web/titanium/card/card.ts deleted file mode 100644 index 7d6ecc6eb..000000000 --- a/packages/web/titanium/card/card.ts +++ /dev/null @@ -1,163 +0,0 @@ -import '@material/web/elevation/elevation'; - -import { css, html, LitElement } from 'lit'; -import { property, customElement, queryAssignedElements } from 'lit/decorators.js'; - -/** - * A material card filled or outlined - * - * @element titanium-card - * - * @cssprop {Number} [--md-elevation-level: 0] - Use md elevation to set elevation levels - * @cssprop {Color} [--md-elevation-shadow-color: --md-sys-color-shadow] - Use md elevation to set elevation levels - * @cssprop {Color} [--md-sys-color-outline-variant] - Card border color - * - * @slot Default - Card content - */ -@customElement('titanium-card') -export class TitaniumCard extends LitElement { - @property({ type: Boolean, reflect: true, attribute: 'has-menu' }) accessor hasMenu: boolean; - @property({ type: Boolean, reflect: true, attribute: 'has-image' }) accessor hasImage: boolean; - @property({ type: Boolean, reflect: true, attribute: 'has-footer' }) accessor hasFooter: boolean; - - @property({ type: Boolean, reflect: true, attribute: 'filled' }) accessor filled: boolean; - @property({ type: Boolean, reflect: true, attribute: 'elevated' }) accessor elevated: boolean; - - @queryAssignedElements({ flatten: true }) - private readonly assignedElements!: HTMLElement[]; - - static styles = [ - css` - :host { - display: grid; - padding: 24px; - gap: 8px 12px; - grid: - 'title' auto - 'body' 1fr; - border-radius: 12px; - position: relative; - - border: 1px solid var(--md-sys-color-outline-variant); - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); - } - - :host([filled]) { - border: none; - background-color: var(--md-sys-color-surface-container); - } - - :host([elevated]) { - border: none; - background-color: var(--md-sys-color-surface-container-low); - --md-elevation-level: 1; - } - - :host([has-menu]) { - grid: - 'title menu' auto - 'body body' 1fr / 1fr auto; - } - - :host([has-footer]) { - grid: - 'title' auto - 'body' 1fr - 'footer' auto; - } - - :host([has-footer][has-menu]) { - grid: - 'title menu' min-content - 'body body' 1fr - 'footer footer' auto / 1fr auto; - } - - :host([has-image]) { - grid: - 'title image' min-content - 'body body' 1fr / 1fr auto; - } - - :host([has-image][has-footer]) { - grid: - 'title image' min-content - 'body body' 1fr - 'footer footer' auto / 1fr auto; - } - - :host([has-image][has-footer][has-menu]) { - grid: - 'title menu' min-content - 'body image' 1fr - 'footer footer' auto / 1fr auto; - } - - ::slotted([card-menu]) { - grid-area: menu; - justify-self: right; - margin: -12px -12px -12px 0; - } - - ::slotted([card-title]) { - grid-area: title; - } - - ::slotted([card-body]) { - grid-area: body; - } - - ::slotted([card-image]) { - grid-area: image; - } - - ::slotted([full-width]) { - margin: 0 -24px; - } - - ::slotted([card-footer]) { - grid-area: footer; - } - - ::slotted([nav]) { - margin: 16px -12px -12px 0; - justify-self: right; - display: flex; - gap: 12px; - align-items: center; - } - - @media (max-width: 400px) { - ::slotted([card-image]) { - display: none; - } - - :host([has-image][has-footer]) { - grid: - 'title' min-content - 'body' auto - 'footer' 1fr / auto; - } - - :host([has-image]) { - grid: - 'title' min-content - 'body' auto / auto; - } - } - `, - ]; - - render() { - return html` - { - this.hasFooter = this.assignedElements.some((el) => el.hasAttribute('card-footer')); - this.hasImage = this.assignedElements.some((el) => el.hasAttribute('card-image')); - this.hasMenu = this.assignedElements.some((el) => el.hasAttribute('card-menu')); - }} - > - `; - } -} diff --git a/packages/web/titanium/chip-multi-select/chip-multi-select.ts b/packages/web/titanium/chip-multi-select/chip-multi-select.ts index 4369c808d..48b82b72e 100644 --- a/packages/web/titanium/chip-multi-select/chip-multi-select.ts +++ b/packages/web/titanium/chip-multi-select/chip-multi-select.ts @@ -1,30 +1,25 @@ import { css, LitElement, PropertyValues } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; -import { literal, html } from 'lit/static-html.js'; +import { html } from 'lit'; -import '../../titanium/input-validator/outlined-input-validator'; import '../../titanium/input-validator/filled-input-validator'; /** - * Multi select outlined themed input that styles + * Multi select filled themed input that styles * slotted in chips and add button * * @element titanium-chip-multi-select * - * @slot default - Main slot (intended to be a <md-outlined-button> and a list of <md-chip>) + * @slot default - Main slot (intended to be a <md-filled-tonal-button> and a list of <md-chip>) * */ @customElement('titanium-chip-multi-select') export class TitaniumChipMultiSelect extends LitElement { - /** - * Swaps outlined validator for filled validator - */ - @property({ type: Boolean, attribute: 'filled' }) accessor filled: boolean = false; /** * Label of input to display to users */ - @property({ type: String }) accessor label: string; + @property({ type: String }) accessor label: string = ''; /** * Text to show when there are no items @@ -39,34 +34,34 @@ export class TitaniumChipMultiSelect extends LitElement { /** * Indicates whether or not to show the no items text */ - @property({ type: Boolean }) accessor hasItems: boolean; + @property({ type: Boolean }) accessor hasItems: boolean = false; /** * Passes the supportingText property to the input-validator */ - @property({ type: String }) accessor supportingText: string; + @property({ type: String }) accessor supportingText: string = ''; /** * Passes the error property to the input-validator */ - @property({ type: Boolean }) accessor error: boolean; + @property({ type: Boolean }) accessor error: boolean = false; /** * Passes the errorText property to the input-validator */ - @property({ type: String }) accessor errorText: string; + @property({ type: String }) accessor errorText: string = ''; /** * Passes the resizable property to the input-validator */ - @property({ type: Boolean }) accessor resizable: boolean; + @property({ type: Boolean }) accessor resizable: boolean = false; /** * Whether or not the input should appear disabled (chips, buttons and anything else slotted will still have to be disabled individually). */ - @property({ type: Boolean, reflect: true }) accessor disabled: boolean; + @property({ type: Boolean, reflect: true }) accessor disabled: boolean = false; - @query('titanium-outlined-input-validator, titanium-filled-input-validator') private accessor validator: + @query('titanium-filled-input-validator') private accessor validator: | { checkValidity: () => boolean; reportValidity: () => boolean; reset: () => void } | undefined; @@ -102,12 +97,20 @@ export class TitaniumChipMultiSelect extends LitElement { :host { display: block; width: 100%; + + --md-filled-field-container-shape: 16px; + + --md-filled-field-active-indicator-height: 0; + --md-filled-field-error-active-indicator-height: 0; + --md-filled-field-hover-active-indicator-height: 0; + --md-filled-field-focus-active-indicator-height: 0; + --md-filled-field-disabled-active-indicator-height: 0; } - titanium-outlined-input-validator, titanium-filled-input-validator { display: block; width: 100%; + --md-filled-field-with-label-bottom-space: 12px; } slot-container { @@ -115,59 +118,41 @@ export class TitaniumChipMultiSelect extends LitElement { flex-wrap: wrap; gap: 8px; align-items: center; + margin-top: 6px; } span { font-size: 13px; } - :host([filled]) { - --md-filled-field-container-shape: 16px; - - --md-filled-field-active-indicator-height: 0; - --md-filled-field-error-active-indicator-height: 0; - --md-filled-field-hover-active-indicator-height: 0; - --md-filled-field-focus-active-indicator-height: 0; - --md-filled-field-disabled-active-indicator-height: 0; + ::slotted(md-filled-button), + ::slotted(md-filled-tonal-button) { + --md-filled-button-container-shape: 8px; + --md-filled-button-container-height: 32px; + + --md-filled-button-with-trailing-icon-leading-space: 8px; + --md-filled-button-with-trailing-icon-trailing-space: 16px; + --md-filled-button-with-leading-icon-leading-space: 8px; + --md-filled-button-with-leading-icon-trailing-space: 16px; + + --md-filled-tonal-button-with-trailing-icon-leading-space: 8px; + --md-filled-tonal-button-with-trailing-icon-trailing-space: 16px; + --md-filled-tonal-button-with-leading-icon-leading-space: 8px; + --md-filled-tonal-button-with-leading-icon-trailing-space: 16px; + --md-filled-tonal-button-container-shape: 8px; + --md-filled-tonal-button-container-height: 32px; + } - slot-container { - margin-top: 6px; - } - - titanium-filled-input-validator { - --md-filled-field-with-label-bottom-space: 12px; - } - - ::slotted(md-filled-button), - ::slotted(md-filled-tonal-button) { - --md-filled-button-container-shape: 8px; - --md-filled-button-container-height: 32px; - - --md-filled-button-with-trailing-icon-leading-space: 8px; - --md-filled-button-with-trailing-icon-trailing-space: 16px; - --md-filled-button-with-leading-icon-leading-space: 8px; - --md-filled-button-with-leading-icon-trailing-space: 16px; - - --md-filled-tonal-button-with-trailing-icon-leading-space: 8px; - --md-filled-tonal-button-with-trailing-icon-trailing-space: 16px; - --md-filled-tonal-button-with-leading-icon-leading-space: 8px; - --md-filled-tonal-button-with-leading-icon-trailing-space: 16px; - --md-filled-tonal-button-container-shape: 8px; - --md-filled-tonal-button-container-height: 32px; - } - - ::slotted(md-input-chip) { - background: var(--md-sys-color-surface-container); - --md-sys-color-outline: transparent; - } + ::slotted(md-input-chip) { + background: var(--md-sys-color-surface-container); + --md-sys-color-outline: transparent; } `, ]; protected render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html` - <${this.filled ? literal`titanium-filled-input-validator` : literal`titanium-outlined-input-validator`} + !this.required || !!this.hasItems} ?required=${this.required} @@ -181,8 +166,7 @@ export class TitaniumChipMultiSelect extends LitElement { ${!this.hasItems ? html` ${this.noItemsText}` : ''} - + `; - /* eslint-enable lit/binding-positions, lit/no-invalid-html */ } } diff --git a/packages/web/titanium/chip/chip.ts b/packages/web/titanium/chip/chip.ts index c55229bb4..71cf63a67 100644 --- a/packages/web/titanium/chip/chip.ts +++ b/packages/web/titanium/chip/chip.ts @@ -13,12 +13,12 @@ export class TitaniumChip extends LitElement { /** * Label / text of the chip */ - @property({ type: String }) accessor label: string; + @property({ type: String }) accessor label: string = ''; /** * When true, the chip is selected */ - @property({ type: Boolean, reflect: true }) accessor selected: boolean; + @property({ type: Boolean, reflect: true }) accessor selected: boolean = false; /** * The URL that the link button points to. @@ -41,12 +41,12 @@ export class TitaniumChip extends LitElement { /** * When true, trailing slot is replaced with a remove icon button */ - @property({ type: Boolean, reflect: true, attribute: 'input-chip' }) accessor inputChip: boolean; + @property({ type: Boolean, reflect: true, attribute: 'input-chip' }) accessor inputChip: boolean = false; /** * Prevents mouse events and disables the ripple effect */ - @property({ type: Boolean, reflect: true, attribute: 'non-interactive' }) accessor nonInteractive: boolean; + @property({ type: Boolean, reflect: true, attribute: 'non-interactive' }) accessor nonInteractive: boolean = false; /** * Icon name of the remove icon chip @@ -58,8 +58,6 @@ export class TitaniumChip extends LitElement { */ @property({ type: Boolean, reflect: true }) accessor disabled: boolean = false; - @property({ type: Boolean }) accessor filled: boolean = false; - @property({ type: Boolean, reflect: true, attribute: 'has-leading-items' }) private accessor hasLeadingItems = false; @property({ type: Boolean, reflect: true, attribute: 'has-trailing-items' }) private accessor hasTrailingItems = false; @@ -100,12 +98,12 @@ export class TitaniumChip extends LitElement { height: inherit; text-align: inherit; - border: 1px solid var(--titanium-chip-outline-color, var(--md-sys-color-outline)); + border: none; border-radius: 8px; --md-focus-ring-shape: 8px; - color: inherit; - background: inherit; + background-color: var(--titanium-chip-filled-background-color, var(--md-sys-color-surface-container)); + color: var(--titanium-chip-filled-color, var(--md-sys-color-on-surface)); width: inherit; outline: none; @@ -119,13 +117,6 @@ export class TitaniumChip extends LitElement { padding: 0 12px; } - :host([filled]) a, - :host([filled]) button { - border: none; - background-color: var(--titanium-chip-filled-background-color, var(--md-sys-color-surface-container)); - color: var(--titanium-chip-filled-color, var(--md-sys-color-on-surface)); - } - :host([selected]) button, :host([selected]) a, :host([has-leading-items]) button, diff --git a/packages/web/titanium/circle-loading-indicator/circle-loading-indicator.ts b/packages/web/titanium/circle-loading-indicator/circle-loading-indicator.ts index b692a70eb..7417cb3f1 100644 --- a/packages/web/titanium/circle-loading-indicator/circle-loading-indicator.ts +++ b/packages/web/titanium/circle-loading-indicator/circle-loading-indicator.ts @@ -13,13 +13,13 @@ import { PendingStateEvent } from '@leavittsoftware/web/titanium/types/pending-s */ @customElement('titanium-circle-loading-indicator') export class TitaniumCircleLoadingIndicator extends LitElement { - @property({ type: Object }) accessor pendingStateElement: Element | null; + @property({ type: Object }) accessor pendingStateElement: Element | null = null; - @property({ type: Boolean, reflect: true }) private accessor open: boolean; - @property({ type: Boolean, reflect: true }) private accessor closed: boolean; + @property({ type: Boolean, reflect: true }) private accessor open: boolean = false; + @property({ type: Boolean, reflect: true }) private accessor closed: boolean = false; - #openDelayTimer: number; - #closeDelayTimer: number; + #openDelayTimer!: number; + #closeDelayTimer!: number; //Promises faster than this do not cause the scrim to open at all //Prevents flicker for fast promises @@ -27,12 +27,12 @@ export class TitaniumCircleLoadingIndicator extends LitElement { // min time scrim has to remain open #minTimeOpen: number = 400; - #timeOpen: number; + #timeOpen!: number; #openCount = 0; firstUpdated() { const element = this.pendingStateElement ?? this; - element.addEventListener(PendingStateEvent.eventType, async (e: PendingStateEvent) => { + element.addEventListener(PendingStateEvent.eventType, (async (e: PendingStateEvent) => { e.stopPropagation(); this.#open(); this.#openCount++; @@ -46,7 +46,7 @@ export class TitaniumCircleLoadingIndicator extends LitElement { this.#close(); } } - }); + }) as unknown as EventListener); } #open() { diff --git a/packages/web/titanium/confirm-dialog/confirm-dialog-open-event.ts b/packages/web/titanium/confirm-dialog/confirm-dialog-open-event.ts deleted file mode 100644 index e4c385dc4..000000000 --- a/packages/web/titanium/confirm-dialog/confirm-dialog-open-event.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TemplateResult } from 'lit'; - -export class ConfirmDialogOpenEvent extends Event { - static eventType = 'confirm-dialog-open'; - header: string; - text: string | TemplateResult; - dialogResult: Promise; - resolver: (confirmed: boolean) => void; - - constructor(header: string, text: string | TemplateResult) { - super(ConfirmDialogOpenEvent.eventType, { bubbles: true, composed: true }); - this.header = header; - this.text = text; - this.dialogResult = new Promise((res) => { - this.resolver = res; - }); - } -} diff --git a/packages/web/titanium/confirm-dialog/confirm-dialog.ts b/packages/web/titanium/confirm-dialog/confirm-dialog.ts deleted file mode 100644 index e8cac1bbb..000000000 --- a/packages/web/titanium/confirm-dialog/confirm-dialog.ts +++ /dev/null @@ -1,95 +0,0 @@ -import '@material/web/dialog/dialog'; -import '@material/web/button/text-button'; - -import { css, html, LitElement, TemplateResult } from 'lit'; -import { customElement, query, state } from 'lit/decorators.js'; -import { ConfirmDialogOpenEvent } from './confirm-dialog-open-event'; -import { MdDialog } from '@material/web/dialog/dialog'; -import { DOMEvent } from '../types/dom-event'; -import { p } from '../styles/p'; -import { dialogZIndexHack } from '../hacks/dialog-zindex-hack'; -import { dialogCloseNavigationHack, dialogOpenNavigationHack } from '../hacks/dialog-navigation-hack'; - -@customElement('titanium-confirm-dialog') -export default class TitaniumConfirmDialog extends LitElement { - @state() protected accessor text: string | TemplateResult; - @state() protected accessor headline: string; - @query('md-dialog') protected accessor dialog!: MdDialog; - #resolve: (value: 'confirmed' | 'cancel') => void; - - /** - * This method is used to set up the event listener to capture the confirm dialog open event - * You can skip using this method and set up the event listener yourself (recommended) - */ - listenOn(el: HTMLElement) { - el.addEventListener(ConfirmDialogOpenEvent.eventType, async (event: ConfirmDialogOpenEvent) => { - this.#openDialogWithEvent(event); - }); - } - - /** - * This method is used after capturing the confirm dialog open event - * usually in the same class where the confirm-dialog element is used. - * After capturing the event pass it directly into this method `this.dialog.handleEvent(e);` - */ - async handleEvent(event: ConfirmDialogOpenEvent) { - this.#openDialogWithEvent(event); - } - - #openDialogWithEvent = async (event: ConfirmDialogOpenEvent) => { - this.headline = event.header; - this.text = event.text; - this.dialog.returnValue = ''; - this.dialog.show(); - - const result = await new Promise<'confirmed' | 'cancel'>((resolve) => { - this.#resolve = resolve; - }); - - event.resolver(result === 'confirmed'); - }; - - static styles = [ - p, - css` - p { - margin: 6px 24px 0 24px; - } - - md-dialog { - max-width: 550px; - max-height: calc(100vh - 24px); - } - - b, - strong { - font-weight: 500; - } - `, - ]; - - render() { - return html` - ) => { - dialogZIndexHack(e.target); - dialogOpenNavigationHack(e.target); - }} - @close=${(e: DOMEvent) => { - dialogCloseNavigationHack(e.target); - if (e.target.returnValue === 'confirmed') { - return this.#resolve('confirmed'); - } - return this.#resolve('cancel'); - }} - > -
${this.headline}
-

${this.text}

-
- this.dialog.close('cancel')}>Cancel - this.dialog.close('confirmed')}>Confirm -
-
- `; - } -} diff --git a/packages/web/titanium/confirmation-dialog/confirmation-dialog.ts b/packages/web/titanium/confirmation-dialog/confirmation-dialog.ts index a3e0c0d0a..6cb3ef497 100644 --- a/packages/web/titanium/confirmation-dialog/confirmation-dialog.ts +++ b/packages/web/titanium/confirmation-dialog/confirmation-dialog.ts @@ -12,8 +12,8 @@ import { dialogCloseNavigationHack, dialogOpenNavigationHack } from '../hacks/di @customElement('titanium-confirmation-dialog') export default class TitaniumConfirmationDialog extends LitElement { - @property({ type: String }) accessor text: string; - @property({ type: String }) accessor headline: string; + @property({ type: String }) accessor text: string = ''; + @property({ type: String }) accessor headline: string = ''; @property({ type: String, attribute: 'confirm-action-text' }) accessor confirmActionText: string = 'Confirm'; @property({ type: String, attribute: 'cancel-action-text' }) accessor cancelActionText: string = 'Cancel'; @property({ type: Boolean, attribute: 'disable-confirmation-action' }) accessor disableConfirmationAction: boolean = false; @@ -21,7 +21,7 @@ export default class TitaniumConfirmationDialog extends LitElement { @query('md-dialog') protected accessor dialog!: MdDialog; - #resolve: (value: 'confirmed' | 'cancel') => void; + #resolve!: (value: 'confirmed' | 'cancel') => void; open = async (headline: string, text: string) => { this.headline = headline; diff --git a/packages/web/titanium/data-table/data-table-action-bar.ts b/packages/web/titanium/data-table/data-table-action-bar.ts index c5e724292..6b0f26d5c 100644 --- a/packages/web/titanium/data-table/data-table-action-bar.ts +++ b/packages/web/titanium/data-table/data-table-action-bar.ts @@ -9,7 +9,7 @@ export class TitaniumDataTableActionBar extends LitElement { @property({ type: Array }) accessor selected: Array> = []; @property({ type: Boolean, reflect: true, attribute: 'has-selected' }) private accessor hasSelected: boolean = false; - @property({ type: Boolean, reflect: true, attribute: 'has-add-button' }) private accessor hasAddButton: boolean; + @property({ type: Boolean, reflect: true, attribute: 'has-add-button' }) private accessor hasAddButton: boolean = false; @queryAssignedElements({ slot: 'add-button' }) private accessor addButtonElements!: Element[]; @@ -49,10 +49,6 @@ export class TitaniumDataTableActionBar extends LitElement { } } - :host([has-search][has-add-button]) div[add-button] { - margin: 0 0 0 12px; - } - selected-action-veil { display: none; grid: 'text buttons' / minmax(min-content, 1fr) auto; diff --git a/packages/web/titanium/data-table/data-table-core-reorder-dialog.ts b/packages/web/titanium/data-table/data-table-core-reorder-dialog.ts index 7aa40f278..4e67d5e6b 100644 --- a/packages/web/titanium/data-table/data-table-core-reorder-dialog.ts +++ b/packages/web/titanium/data-table/data-table-core-reorder-dialog.ts @@ -17,18 +17,24 @@ import { DOMEvent } from '../types/dom-event'; import { MdDialog } from '@material/web/dialog/dialog'; import { ItemDropEvent } from './draggable-item-base'; import { repeat } from 'lit/directives/repeat.js'; -import { LoadWhile } from '../helpers/load-while'; +import { promiseTracking } from '../helpers/promise-tracking'; import { ShowSnackbarEvent } from '../snackbar/show-snackbar-event'; import { SnackbarStack } from '@leavittsoftware/web/titanium/snackbar/snackbar-stack'; +import { HttpError } from '@leavittsoftware/web/leavitt/api-service/HttpError'; export type CloseReason = 'apply' | 'cancel'; @customElement('titanium-data-table-core-reorder-dialog') -export class TitaniumDataTableCoreReorderDialog extends LoadWhile(LitElement) { +export class TitaniumDataTableCoreReorderDialog extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + @property({ type: Object }) accessor tableMetaData: TitaniumDataTableCoreMetaData | null = null; @property({ type: Object }) accessor supplementalItemStyles: CSSResult | CSSResultGroup | null = null; - @query('md-dialog') private accessor dialog: MdDialog; + @query('md-dialog') private accessor dialog!: MdDialog; @query('titanium-snackbar-stack') private accessor snackbar!: SnackbarStack; @state() accessor items: Array = []; @@ -61,7 +67,7 @@ export class TitaniumDataTableCoreReorderDialog extends LoadWh return JSON.stringify(sortA) !== JSON.stringify(sortB); } - #resolve: (value: CloseReason) => void; + #resolve!: (value: CloseReason) => void; static styles = [ css` :host { @@ -148,7 +154,7 @@ export class TitaniumDataTableCoreReorderDialog extends LoadWh _resolve = resolve; _reject = reject; }); - this.loadWhile(saving); + this.trackLoadingPromise(saving); this.dispatchEvent( new CustomEvent<{ resolve: () => void; reject: (reason: any) => void; items: Array }>('reorder-save-request', { detail: { resolve: _resolve, reject: _reject, items: this.items }, @@ -159,7 +165,7 @@ export class TitaniumDataTableCoreReorderDialog extends LoadWh await saving; this.dialog?.close('apply'); } catch (error) { - this.dispatchEvent(new ShowSnackbarEvent(error)); + this.dispatchEvent(new ShowSnackbarEvent(error as Partial)); } }} >Save diff --git a/packages/web/titanium/data-table/data-table-core-reorder-item.ts b/packages/web/titanium/data-table/data-table-core-reorder-item.ts index 1111f069d..ee2eaaf0c 100644 --- a/packages/web/titanium/data-table/data-table-core-reorder-item.ts +++ b/packages/web/titanium/data-table/data-table-core-reorder-item.ts @@ -9,7 +9,7 @@ import { dataTableContentStyles } from './data-table-content-styles'; @customElement('titanium-data-table-core-reorder-item') export class TitaniumDataTableCoreReorderItem extends DraggableItemBase { - @property({ type: Object }) accessor item: T; + @property({ type: Object }) accessor item!: T; @property({ type: Object }) accessor tableMetaData: TitaniumDataTableCoreMetaData | null = null; @property({ type: Object }) accessor supplementalItemStyles: CSSResult | CSSResultGroup | null = null; diff --git a/packages/web/titanium/data-table/data-table-core-settings-choose-columns-dialog.ts b/packages/web/titanium/data-table/data-table-core-settings-choose-columns-dialog.ts index 491dd722f..a4ed8dde7 100644 --- a/packages/web/titanium/data-table/data-table-core-settings-choose-columns-dialog.ts +++ b/packages/web/titanium/data-table/data-table-core-settings-choose-columns-dialog.ts @@ -26,7 +26,7 @@ export class TitaniumDataTableCoreSettingsChooseColumnsDialog @state() accessor customColumnsApplied: boolean = false; - @query('md-dialog') private accessor dialog: MdDialog; + @query('md-dialog') private accessor dialog!: MdDialog; updated(changedProperties: PropertyValues) { if (changedProperties.has('tableMetaData')) { @@ -90,7 +90,7 @@ export class TitaniumDataTableCoreSettingsChooseColumnsDialog }); } - #resolve: (value: 'done') => void; + #resolve!: (value: 'done') => void; static styles = [ css` :host { diff --git a/packages/web/titanium/data-table/data-table-core-settings-choose-columns-item.ts b/packages/web/titanium/data-table/data-table-core-settings-choose-columns-item.ts index 3727c33a5..c882530e4 100644 --- a/packages/web/titanium/data-table/data-table-core-settings-choose-columns-item.ts +++ b/packages/web/titanium/data-table/data-table-core-settings-choose-columns-item.ts @@ -8,9 +8,9 @@ import { DraggableItemBase } from './draggable-item-base'; @customElement('titanium-data-table-core-settings-choose-columns-item') export class TitaniumDataTableCoreSettingsChooseColumnsItem extends DraggableItemBase { - @property({ type: String }) accessor name: string; - @property({ type: Boolean }) accessor selected: boolean; - @property({ type: Boolean }) accessor disabled: boolean; + @property({ type: String }) accessor name: string = ''; + @property({ type: Boolean }) accessor selected: boolean = false; + @property({ type: Boolean }) accessor disabled: boolean = false; override get items() { return Array.from( diff --git a/packages/web/titanium/data-table/data-table-core-settings-sort-dialog.ts b/packages/web/titanium/data-table/data-table-core-settings-sort-dialog.ts index 2989610c1..06e0f7b79 100644 --- a/packages/web/titanium/data-table/data-table-core-settings-sort-dialog.ts +++ b/packages/web/titanium/data-table/data-table-core-settings-sort-dialog.ts @@ -26,8 +26,8 @@ export type CloseReason = 'apply' | 'cancel' | 'navigation-close'; @customElement('titanium-data-table-core-settings-sort-dialog') export class TitaniumDataTableCoreSettingsSortDialog extends LitElement { @property({ type: Object }) accessor tableMetaData: TitaniumDataTableCoreMetaData | null = null; - @query('md-dialog') private accessor dialog: MdDialog; - @query('md-menu') private accessor addMenu: MdMenu; + @query('md-dialog') private accessor dialog!: MdDialog; + @query('md-menu') private accessor addMenu!: MdMenu; @state() accessor sort: TitaniumDataTableCoreSortItem[] = []; #originalSort: TitaniumDataTableCoreSortItem[] = []; @@ -47,8 +47,8 @@ export class TitaniumDataTableCoreSettingsSortDialog extends L return JSON.stringify(sortA) !== JSON.stringify(sortB); } - #repositionMenu: EventListener; - #resolve: (value: CloseReason) => void; + #repositionMenu!: EventListener; + #resolve!: (value: CloseReason) => void; static styles = [ p, niceBadgeStyles, diff --git a/packages/web/titanium/data-table/data-table-core-settings-sort-item.ts b/packages/web/titanium/data-table/data-table-core-settings-sort-item.ts index a33e684fa..b441c5d6a 100644 --- a/packages/web/titanium/data-table/data-table-core-settings-sort-item.ts +++ b/packages/web/titanium/data-table/data-table-core-settings-sort-item.ts @@ -13,10 +13,10 @@ import { DraggableItemBase } from './draggable-item-base'; @customElement('data-table-core-settings-sort-item') export class TitaniumDataTableCoreSettingsSortItem extends DraggableItemBase { - @property({ type: String }) accessor name: string; - @property({ type: String, reflect: true, attribute: 'sort-direction' }) accessor sortDirection: 'asc' | 'desc'; - @property({ type: Number }) accessor index: number; - @property({ type: Boolean, reflect: true }) accessor disabled: boolean; + @property({ type: String }) accessor name: string = ''; + @property({ type: String, reflect: true, attribute: 'sort-direction' }) accessor sortDirection!: 'asc' | 'desc'; + @property({ type: Number }) accessor index: number = 0; + @property({ type: Boolean, reflect: true }) accessor disabled: boolean = false; override get items() { return Array.from(this.parentElement?.querySelectorAll('data-table-core-settings-sort-item') ?? []); diff --git a/packages/web/titanium/data-table/data-table-core.ts b/packages/web/titanium/data-table/data-table-core.ts index 3701bf003..39062962e 100644 --- a/packages/web/titanium/data-table/data-table-core.ts +++ b/packages/web/titanium/data-table/data-table-core.ts @@ -21,7 +21,7 @@ import { MdCheckbox } from '@material/web/checkbox/checkbox'; import { repeat } from 'lit/directives/repeat.js'; import { a } from '@leavittsoftware/web/titanium/styles/a'; import { styleMap } from 'lit/directives/style-map.js'; -import { LoadWhile } from '@leavittsoftware/web/titanium/helpers/load-while'; +import { promiseTracking } from '@leavittsoftware/web/titanium/helpers/promise-tracking'; import { niceBadgeStyles } from '../styles/nice-badge'; import { MdIconButton } from '@material/web/iconbutton/icon-button'; import { CloseMenuEvent, MdMenu, MenuItem } from '@material/web/menu/menu'; @@ -78,7 +78,12 @@ export function generateDefaultSortFromMetaData(tableMetaData: } @customElement('titanium-data-table-core') -export class TitaniumDataTableCore extends LoadWhile(LitElement) { +export class TitaniumDataTableCore extends LitElement { + @promiseTracking('trackLoadingPromise') + @state() + accessor isLoading = false; + declare trackLoadingPromise: (promise: Promise) => Promise; + /** * Current items displayed on the table. */ @@ -102,9 +107,9 @@ export class TitaniumDataTableCore extends LoadWhile(LitElemen */ @property({ type: Array }) accessor selected: Array = []; - @query('titanium-data-table-core-settings-choose-columns-dialog') private accessor chooseColumnsDialog: TitaniumDataTableCoreSettingsChooseColumnsDialog; - @query('titanium-data-table-core-settings-sort-dialog') private accessor sortDialog: TitaniumDataTableCoreSettingsSortDialog; - @query('titanium-data-table-core-reorder-dialog') private accessor reorderDialog: TitaniumDataTableCoreReorderDialog; + @query('titanium-data-table-core-settings-choose-columns-dialog') private accessor chooseColumnsDialog!: TitaniumDataTableCoreSettingsChooseColumnsDialog; + @query('titanium-data-table-core-settings-sort-dialog') private accessor sortDialog!: TitaniumDataTableCoreSettingsSortDialog; + @query('titanium-data-table-core-reorder-dialog') private accessor reorderDialog!: TitaniumDataTableCoreReorderDialog; /** * Local storage key to save user settings for this data table. */ @@ -173,6 +178,11 @@ export class TitaniumDataTableCore extends LoadWhile(LitElemen } } + /** @deprecated Use trackLoadingPromise. Alias kept for downstream compat. */ + loadWhile(promise: Promise) { + return this.trackLoadingPromise(promise); + } + #notifySelectedChanged() { this.dispatchEvent(new Event('selected-changed', { composed: true })); } diff --git a/packages/web/titanium/data-table/data-table-header.ts b/packages/web/titanium/data-table/data-table-header.ts deleted file mode 100644 index 3604039ee..000000000 --- a/packages/web/titanium/data-table/data-table-header.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { css, html, LitElement, PropertyValues } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; - -import '@material/web/icon/icon'; -import '@material/web/ripple/ripple'; -import '@material/web/focus/md-focus-ring'; - -/** - * Material design data table header with styling and sorting capabilities - * - * @element titanium-data-table-header - * - * @fires sort-direction-changed - Fired if sort direction is changed (detail: 'desc' | 'asc') - * @fires sort-by-changed - Fired when the close button is clicked (detail: {string} column name of currently sorted header ) - * - * @cssprop {font} [--titanium-data-table-font-family=Roboto, Noto, sans-serif] - Font family - */ -@customElement('titanium-data-table-header') -export class TitaniumDataTableHeader extends LitElement { - /** - * This displayed header name - */ - @property({ type: String }) accessor title: string; - - /** - * The column name of the currently applied sort - */ - @property({ type: String }) accessor sortBy: string; - - /** - * Optional fixed width of header in px ex. "140px" - */ - @property({ reflect: true, type: String }) accessor width: string; - - /** - * True if header is currently the sorted column. Read-only, do not set. - */ - @property({ type: Boolean, reflect: true }) accessor active: boolean = false; - - /** - * Current sort direction on header. - */ - @property({ type: String, reflect: true, attribute: 'sort-direction' }) accessor sortDirection: 'asc' | 'desc' | ''; - - /** - * Name of header column passed along in sort-by-changed event. Typically the name of the col in the backing DB. ex. first_name - */ - @property({ type: String, attribute: 'column-name' }) accessor columnName: string; - - /** - * Justify header text center - */ - @property({ type: Boolean, reflect: true }) accessor center: boolean = false; - - /** - * Justify header text right; moves sort icon to left. - */ - @property({ type: Boolean, reflect: true }) accessor right: boolean = false; - - /** - * Removes the sort icon - */ - @property({ type: Boolean, reflect: true, attribute: 'no-sort' }) accessor noSort: boolean = false; - - /** - * Set flex 5 on header, default is 3. - */ - @property({ type: Boolean, reflect: true }) accessor large: boolean = false; - - /** - * Only show this header when width is larger - */ - @property({ type: Boolean, reflect: true }) accessor desktop: boolean = false; - - /** - * Sets if view port is small - */ - @property({ type: Boolean, reflect: true }) accessor narrow: boolean = false; - - updated(changedProps: PropertyValues) { - if (changedProps.has('sortBy') && changedProps.get('sortBy') !== this.sortBy) { - this.active = this.sortBy === this.columnName; - } - - if (changedProps.has('width') && changedProps.get('width') !== this.width && this.width) { - this.style.width = this.width; - } - } - - static styles = css` - :host { - display: flex; - - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - - text-align: left; - } - - button { - display: flex; - flex-direction: row; - align-items: center; - - position: relative; - --md-focus-ring-shape: 0; - - font-family: var(--titanium-data-table-font-family, Roboto, Noto, sans-serif); - font-size: 14px; - padding: 8px; - - /* cancel padding so text aligns */ - margin: 0 -8px; - - line-height: 28px; - font-weight: 500; - height: 100%; - - /* override default button styles */ - text-align: inherit; - - cursor: pointer; - - background-color: inherit; - color: inherit; - - border: none; - outline: none; - } - - :host([right]) { - justify-content: end; - text-align: right; - } - - :host([center]) { - justify-content: center; - text-align: center; - } - - button:focus, - button:active { - outline: none; - box-shadow: none; - } - - :host(:not([width])) { - -ms-flex: 3; - -webkit-flex: 3; - flex: 3; - } - - :host(:not([width])[large]) { - -ms-flex: 5; - -webkit-flex: 5; - flex: 5; - } - - :host([hidden]) { - display: none !important; - } - - :host([no-sort]) button { - cursor: inherit; - } - - :host([center]) button { - margin-left: 22px; - } - - :host([right]) button { - flex-direction: row-reverse; - } - - md-icon { - display: block; - height: 18px; - width: 18px; - font-size: 18px; - margin-left: 4px; - flex-shrink: 0; - transform-origin: center; - transition: transform 150ms ease; - } - - :host([no-sort]) button md-icon { - display: none; - } - - :host([right]) md-icon { - display: block; - margin-left: 0; - margin-right: 4px; - } - - :host([width]) span { - word-break: break-all; - } - - md-icon { - visibility: hidden; - } - - :host([active][sort-direction='asc']) md-icon { - transform: rotate(-180deg); - } - - :host([active][sort-direction='asc']) md-icon, - :host([active][sort-direction='desc']) md-icon { - visibility: visible; - } - - :host([narrow][desktop]) { - display: none; - } - `; - - render() { - return html` - - `; - } -} diff --git a/packages/web/titanium/data-table/data-table-item.ts b/packages/web/titanium/data-table/data-table-item.ts deleted file mode 100644 index a787ddbcd..000000000 --- a/packages/web/titanium/data-table/data-table-item.ts +++ /dev/null @@ -1,540 +0,0 @@ -import { css, html, LitElement, nothing, PropertyValues } from 'lit'; -import { property, customElement, state } from 'lit/decorators.js'; -import { TitaniumDataTable } from './data-table'; - -import '@material/web/checkbox/checkbox'; -import '@material/web/icon/icon'; - -/** - * A data table element to organize row data and handle row selection. - * - * row-item positioning attributes: - * - right - * - desktop - * - large - * - center - * - width - ex. "140px" - * - * @element titanium-data-table-item - * - * @fires titanium-data-table-item-navigate - Fired on double click of a row. detail: unknown(this.item) - * @fires titanium-data-table-item-selected-changed - Fired when item is selected. detail: { isSelected: boolean, item: unknown } - * @fires titanium-data-table-item-drop - Fired when item is dropped after a drag - * - * @slot default - Main slot that should contain a list of row-item elements - * @slot item-footer - Optional footer content below the row with the row-items - * - * @cssprop {Color} [--md-sys-color-secondary-container] - Row selected color - * @cssprop {Color} [--md-sys-color-on-surface] - Row hover color - * @cssprop {Color} [--md-sys-color-outline-variant] - Bottom division line - * @cssprop [--titanium-data-table-font-family=Roboto, Noto, sans-serif] - Set the font family of the data table item - */ -@customElement('titanium-data-table-item') -export class TitaniumDataTableItem extends LitElement { - /** - * The backing object that is displayed in this row. Sent in navigate and selected events. - */ - @property({ type: Object }) accessor item: unknown; - - /** - * True when row is selected. - */ - @property({ reflect: true, type: Boolean }) accessor selected: boolean = false; - - /** - * Disables ability to select this row. - */ - @property({ type: Boolean, attribute: 'disable-select' }) accessor disableSelect: boolean; - - /** - * Sets if view port is small - */ - @property({ type: Boolean, reflect: true }) accessor narrow: boolean = false; - - /** - * Set to true to make item draggable. When items are dropped, the items in the list's array are sorted accordingly. - * In order to reflect those updates out to the DOM, you will need to call requestUpdate on the items array when - * items are dropped. ex. - * - * this.requestUpdate('items')} ... > - * - */ - @property({ type: Boolean, reflect: true, attribute: 'enable-dragging' }) accessor enableDrag: boolean = false; - - @property({ type: Boolean, reflect: true, attribute: 'nudge-down' }) protected accessor nudgeDown: boolean; - @property({ type: Boolean, reflect: true, attribute: 'nudge-up' }) protected accessor nudgeUp: boolean; - @property({ type: Boolean, reflect: true, attribute: 'dragged' }) protected accessor dragged: boolean; - @property({ type: Boolean, reflect: true, attribute: 'dragging' }) protected accessor dragging: boolean; - - @state() protected accessor nudgeHeight: number; - @state() protected accessor hoverIndex: number | null = null; - @state() protected accessor originIndex: number | null = null; - - protected mouseEvent = (e) => this.#startItemDrag(e, 'mouse'); - protected touchEvent = (e) => { - this.#startItemDrag(e, 'touch'); - }; - - async updated(changed: PropertyValues) { - if (changed.has('enableDrag')) { - if (this.enableDrag) { - this.addEventListener('mousedown', this.mouseEvent); - this.addEventListener('touchstart', this.touchEvent); - } else { - this.removeEventListener('mousedown', this.mouseEvent); - this.removeEventListener('touchstart', this.touchEvent); - } - } - } - - firstUpdated() { - const elements = this.shadowRoot?.querySelector('slot')?.assignedElements(); - elements?.forEach((e) => { - const element = e as HTMLElement; - const width = element.getAttribute('width'); - if (width) { - element.style.width = width; - } - }); - - this.addEventListener('dblclick', () => { - //Force the transition to end on the double-click - /** - * @internal - */ - this.dispatchEvent(new Event('transitionend')); - this.dispatchEvent(new CustomEvent('titanium-data-table-item-navigate', { detail: this.item })); - }); - } - - /** - * @ignore - */ - updateDragProps(dragging: boolean, originIndex: number | null, hoverIndex: number | null, originHeight: number) { - const myIndex = this.items.indexOf(this); - this.nudgeDown = originIndex !== null && hoverIndex !== null && myIndex < originIndex && myIndex >= hoverIndex; - this.nudgeUp = originIndex !== null && hoverIndex !== null && myIndex > originIndex && myIndex <= hoverIndex; - this.dragged = originIndex === myIndex; - this.dragging = dragging; - this.nudgeHeight = originHeight; - } - - /** - * toggles item's checkbox which triggers this.selected to toggle as well - */ - toggleSelected() { - if (this.selected) { - this.deselect(); - } else { - this.select(); - } - } - - /** - * if not already checked, triggers click on checkbox which triggers this.selected to be set as well - */ - select() { - if (!this.selected) { - this.#setSelected(true); - } - } - - /** - * if already checked, triggers click on checkbox which triggers this.selected to be set to false as well - */ - deselect() { - if (this.selected) { - this.#setSelected(false); - } - } - - #setSelected(value: boolean) { - this.selected = value; - this.dispatchEvent(new Event('titanium-data-table-item-selected-changed', { bubbles: true, composed: true })); - } - - protected get dataTable() { - return this.parentElement as TitaniumDataTable; - } - - protected get items() { - return (this.dataTable.itemsSlot?.assignedElements() as TitaniumDataTableItem[]) ?? []; - } - - protected get itemsContainer() { - return this.dataTable.itemsContainer; - } - - /** - * Return index of item over - */ - #getIndexOver(itemEndPositions: number[], hoverPosition: number) { - for (let index = 0; index < itemEndPositions.length; index++) { - const endPosition = itemEndPositions[index]; - if (hoverPosition <= endPosition) { - return index; - } - } - return itemEndPositions.length - 1; - } - - /** - * Given the origin and hover index determine items that will be affected - */ - #determineRange(originIndex: number | null, hoverIndex: number | null) { - //PREF: ONLY UPDATE ITEMS BETWEEN ORIGIN AND HOVER (+1 and -1) - const high = Math.max(hoverIndex ?? 0, originIndex ?? 0) + 1; - let low = Math.min(hoverIndex ?? 0, originIndex ?? 0) - 1; - low = low < 0 ? 0 : low; - - return [high, low]; - } - - #notifySiblingsDrag(originIndex: number | null, hoverIndex: number | null, dragging: boolean, itemHeight: number) { - const range = this.#determineRange(originIndex, hoverIndex); - for (let index = range[1]; index <= range[0]; index++) { - const o = this.items?.[index]; - o?.updateDragProps(dragging, this.originIndex, this.hoverIndex, itemHeight); - } - } - - #notifySiblingDragStop(originIndex: number | null, hoverIndex: number | null) { - const range = this.#determineRange(originIndex, hoverIndex); - for (let index = range[1]; index <= range[0]; index++) { - const o = this.items?.[index]; - o?.updateDragProps(false, null, null, 0); - } - } - - #startItemDrag(event, type: 'touch' | 'mouse') { - //only allow primary mouse for drag - if (type === 'mouse' && event.which !== 1) { - return; - } - - //Prevent native scrolling - event.preventDefault(); - - this.dragging = true; - this.originIndex = this.items.indexOf(this); - - const moveEvent = type === 'touch' ? 'touchmove' : 'mousemove'; - const upEvent = type === 'touch' ? 'touchend' : 'mouseup'; - const containerY = this.itemsContainer?.getBoundingClientRect().top + window.scrollY; - const startY = event.pageY ?? event.touches[0].pageY; - const itemHeight = this.getBoundingClientRect().height - 1; - - //Cache the end positions of each item for variable height list items - let cumulativeSum = 0; - const itemEndPositions = this.items.map((o) => { - cumulativeSum = cumulativeSum + (o.getBoundingClientRect().height - 1); - return cumulativeSum; - }); - - const moveItemHandler = (event) => { - // Translate and keep track of which index we are hovering over. - const pageY = event.pageY ?? event.touches[0].pageY; - const clientY = event.clientY ?? event.touches[0].clientY; - - const itemAbsoluteTop = pageY - containerY; - const transformY = pageY - startY; - - this.style.transform = `translateY(${transformY}px)`; - this.hoverIndex = this.#getIndexOver(itemEndPositions, itemAbsoluteTop); - this.#notifySiblingsDrag(this.originIndex, this.hoverIndex, this.dragging, itemHeight); - - //Scroll on when item approaches bottom/top of viewport - if (clientY < 5) { - scrollBy({ - top: -(window.innerHeight / 5), - behavior: 'smooth', - }); - } else if (clientY < 25) { - scrollBy({ - top: -(window.innerHeight / 10), - behavior: 'smooth', - }); - } - - if (window.innerHeight - clientY < 5) { - scrollBy({ - top: window.innerHeight / 5, - behavior: 'smooth', - }); - } else if (window.innerHeight - clientY < 25) { - scrollBy({ - top: window.innerHeight / 10, - behavior: 'smooth', - }); - } - }; - - const cancelDragHandler = () => { - document.removeEventListener(moveEvent, moveItemHandler); - this.removeEventListener(upEvent, dragCompleteHandler); - this.dragging = false; - - const onTransitionEnd = () => { - this.#notifySiblingDragStop(this.originIndex, this.hoverIndex); - this.originIndex = null; - this.hoverIndex = null; - - this.style.transform = ''; - this.style.transition = ''; - this.removeEventListener('transitionend', onTransitionEnd); - }; - this.addEventListener('transitionend', onTransitionEnd); - - this.style.transition = 'transform 0.1s ease-out'; - this.style.transform = 'translate3d(0, 0, 0)'; - - document.removeEventListener('mouseout', cancelDragHandler); - }; - - const dragCompleteHandler = () => { - this.dragging = false; - this.items.forEach((o) => (o.dragging = false)); - document.removeEventListener(moveEvent, moveItemHandler); - - document.removeEventListener(upEvent, dragCompleteHandler); - if (type === 'mouse') { - document.removeEventListener('mouseout', cancelDragHandler); - } - - // Perform the swap after the item translates to its resting spot. - const onTransitionEnd = () => { - if (this.originIndex !== null && this.hoverIndex !== null) { - /** - * @ignore - */ - this.dispatchEvent(new DataTableItemDropEvent(this.originIndex, this.hoverIndex)); - } - this.#notifySiblingDragStop(this.originIndex, this.hoverIndex); - this.originIndex = null; - this.hoverIndex = null; - - this.style.transform = ''; - this.style.transition = ''; - this.removeEventListener('transitionend', onTransitionEnd); - }; - this.addEventListener('transitionend', onTransitionEnd); - - //Count the nudged items heights to know final transform amount - const finalTransformYUp = this.items - .filter((o) => o.nudgeUp) - .map((o) => (o.getBoundingClientRect().height > 0 ? o.getBoundingClientRect().height - 1 : 0)) - .reduce((a, b) => a + b, 0); - - const finalTransformYDown = this.items - .filter((o) => o.nudgeDown) - .map((o) => -o.getBoundingClientRect().height - 1) - .reduce((a, b) => a + b, 0); - - const finalTransformY = finalTransformYUp !== 0 ? finalTransformYUp : finalTransformYDown; - - // Translate the item to its resting spot. - this.style.transition = 'transform 0.1s ease-out'; - this.style.transform = `translate3d(0, ${finalTransformY}px, 0)`; - }; - - if (type === 'mouse') { - window.addEventListener('mouseout', cancelDragHandler); - } - document.addEventListener(upEvent, dragCompleteHandler); - document.addEventListener(moveEvent, moveItemHandler); - moveItemHandler(event); - } - - static styles = css` - :host { - display: block; - - -webkit-touch-callout: none; - user-select: none; - text-decoration: none; - - font-family: var(--titanium-data-table-font-family, Roboto, Noto, sans-serif); - -webkit-font-smoothing: antialiased; - - transition: none; - margin-top: -1px; - box-sizing: border-box; - border-bottom: 1px var(--md-sys-color-outline-variant) solid; - border-top: 1px var(--md-sys-color-outline-variant) solid; - position: relative; - } - - :host(:not([disable-select])[selected]) { - background-color: var(--md-sys-color-secondary-container); - } - - :host(:not([disable-select]):not([selected]):hover) { - background-color: rgb(from var(--md-sys-color-on-surface, #1d1b20) r g b / 0.08); - } - - :host([enable-dragging]) { - cursor: grab; - } - - md-icon[drag] { - position: absolute; - opacity: 0.3; - right: 7px; - color: var(--md-sys-color-outline, #dadce0); - } - - :host([enable-dragging]:hover) md-icon[drag] { - opacity: 1; - display: block; - } - - :host([dragged]) { - box-shadow: - 0 3px 6px rgba(0, 0, 0, 0.16), - 0 3px 6px rgba(0, 0, 0, 0.23); - transition: none; - overflow: hidden; - z-index: 1 !important; - } - - /* Only have transition under dragging, because we don't want nudged - * items to transition into place once dragging is complete */ - :host([dragging]:not([dragged])) { - transition: transform 0.2s ease-out; - } - - :host main { - display: flex; - flex-direction: row; - gap: 16px; - align-items: center; - min-height: 48px; - } - - /* Fallback :hover style for Firefox support */ - @-moz-document url-prefix() { - :host(:not([disable-select]):not([selected]):hover) { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface, #1d1b20) 8%, transparent); - } - } - - /* Do not apply :hover style on touch devices */ - @media (hover: hover) and (pointer: fine) { - :host([enable-dragging]) div[item-footer] ::slotted(*) { - pointer-events: none; - } - } - - ::slotted(row-item) { - display: block; - font-size: 14px; - line-height: 18px; - font-weight: 400; - padding: 4px 0; - margin: 0; - box-sizing: border-box; - } - - ::slotted(row-item:last-of-type) { - padding-right: 24px; - } - - :host([enable-dragging]) ::slotted(row-item:last-of-type) { - padding-right: 40px; - } - - ::slotted(row-item:not([width])) { - -ms-flex: 3; - -webkit-flex: 3; - flex: 3; - } - - ::slotted(row-item:not([width])[large]) { - -ms-flex: 5; - -webkit-flex: 5; - flex: 5; - } - - ::slotted(row-item[center]) { - text-align: center; - } - - ::slotted(row-item[image]) { - display: inline-flex; - align-items: center; - gap: 12px; - } - - ::slotted(row-item[right]) { - text-align: right; - } - - md-checkbox { - flex-shrink: 0; - align-self: center; - margin: 0 14px 0 20px; - } - - :host([disable-select]) ::slotted(row-item:first-of-type) { - padding-left: 24px; - } - - :host([narrow]) ::slotted(row-item[desktop]) { - display: none; - } - - [hidden] { - display: none; - } - `; - - render() { - return html` - -
- ${this.disableSelect - ? nothing - : html` - e.stopPropagation()} - @touchstart=${(e: TouchEvent) => e.stopPropagation()} - @dblclick=${(e) => e.stopPropagation()} - @click=${(e) => e.stopPropagation()} - @change=${(e) => this.#setSelected(e.target.checked)} - > - `} - - - ${this.enableDrag ? html` drag_indicator` : nothing} -
-
- -
- `; - } -} - -/** - * @class - * @ignore - */ -export class DataTableItemDropEvent extends Event { - static eventType = 'titanium-data-table-item-drop'; - hoverIndex: number; - originIndex: number; - - constructor(originIndex: number, hoverIndex: number) { - super(DataTableItemDropEvent.eventType, { composed: true, bubbles: true }); - this.hoverIndex = hoverIndex; - this.originIndex = originIndex; - } -} diff --git a/packages/web/titanium/data-table/data-table.ts b/packages/web/titanium/data-table/data-table.ts deleted file mode 100644 index 335b27f58..000000000 --- a/packages/web/titanium/data-table/data-table.ts +++ /dev/null @@ -1,626 +0,0 @@ -import './page-control'; -import '@material/web/checkbox/checkbox'; -import '@material/web/progress/linear-progress'; -import '@material/web/icon/icon'; - -import { css, html, LitElement } from 'lit'; -import { property, customElement, query, queryAsync, state } from 'lit/decorators.js'; -import { DataTableItemDropEvent, TitaniumDataTableItem } from './data-table-item'; -import { TitaniumDataTableHeader } from './data-table-header'; -import { h1, ellipsis } from '../../titanium/styles/styles'; -import { TitaniumPageControl } from './page-control'; -import { MdCheckbox } from '@material/web/checkbox/checkbox'; -import { niceBadgeStyles } from '../styles/nice-badge'; - -declare const ResizeObserver: any; - -/** - * Material design inspired data table with paging, sorting, multi/single select, table actions, selected actions and more! - * - * @element titanium-data-table - * - * @fires selected-changed - Fired when a row or rows in the data table is selected. detail: array - * @fires titanium-data-table-items-reorder - Fired when table items are resorted by user. - * @fires paging-changed - Fired when take or page is changed by click or keyboard action. - * - * @slot table-actions - item nonspecific table buttons such as add new item - * @slot filter-button - filter button slot - * @slot filters - filter chips slot - * @slot search-button - search button slot - * @slot selected-actions - item specific table buttons such as edit, delete shown when one or more items are selected - * @slot table-headers - slot for table headers (ex. titanium-data-table-header) - * @slot items - slot for table rows (ex. titanium-data-table-item) - * @slot footer - slot for additional footer items. Slotting here overwrites footer-buttons. - * @slot footer-buttons - slot for footer action buttons - * - * @cssprop {Color} [var(--md-sys-color-outline-variant)] - Table border color - * @cssprop {Color} [--titanium-data-table-font-family=Roboto, Noto, sans-serif] - Set the font family used on the data table and paging control - */ -@customElement('titanium-data-table') -export class TitaniumDataTable extends LitElement { - /** - * Table heading / title - */ - @property({ type: String }) accessor header: string; - - /** - * Local storage key. Not required if header is static and unique - */ - @property({ type: String, attribute: 'local-storage-key' }) accessor localStorageKey: string; - - /** - * Available page sizes - */ - @property({ type: Array }) accessor pageSizes: Array = [10, 15, 20, 50]; - - /** - * The default page size before the user changes it - */ - @property({ type: Number, attribute: 'default-page-size' }) accessor defaultPageSize: number = 10; - - /** - * Total number of items in all pages. - */ - @property({ type: Number }) accessor count: number; - - /** - * Current items displayed on the table. - */ - @property({ type: Array }) accessor items: Array = []; - - /** - * Current search term shown in the no result state if no results are found - */ - @property({ type: String }) accessor searchTerm: string; - - /** - * Limits table selection mode to single-select. Default is multi-select. - */ - @property({ type: Boolean, attribute: 'single-select', reflect: true }) accessor singleSelect: boolean; - - /** - * Disables all item selection on the data-table. - */ - @property({ type: Boolean, attribute: 'disable-select' }) accessor disableSelect: boolean = false; - - /** - * Disables paging. - */ - @property({ type: Boolean, attribute: 'disable-paging', reflect: true }) accessor disablePaging: boolean = false; - - /** - * Array of currently selected data table objects - */ - @property({ type: Array }) accessor selected: Array = []; - @query('div[items-slot]') accessor itemsContainer: HTMLDivElement; - @query('slot[name="items"]') accessor itemsSlot: HTMLSlotElement; - - @property({ type: Boolean, reflect: true, attribute: 'narrow' }) protected accessor narrow: boolean = false; - @property({ type: Boolean, attribute: 'has-drag-items', reflect: true }) protected accessor hasDragItems: boolean; - @property({ type: Number, attribute: 'narrow-max-width', reflect: true }) protected accessor narrowMaxWidth: number = 560; - @state() protected accessor isLoading: boolean; - @query('slot[name="table-headers"]') protected accessor tableHeaders: HTMLSlotElement; - @query('md-checkbox') protected accessor checkbox: MdCheckbox; - @queryAsync('titanium-page-control') protected accessor pageControl: Promise; - #openCount = 0; - - /** - * returns internal pageControl's current take - */ - public async getTake(): Promise { - return (await this.pageControl)?.take ?? 0; - } - - /** - * returns internal pageControl's current page - */ - public async getPage(): Promise { - return (await this.pageControl)?.page ?? 0; - } - - /** - * sets internal pageControl's current take - */ - public async setTake(take: number): Promise { - const control = await this.pageControl; - if (control) { - control.take = take; - } - } - - /** - * sets internal pageControl's current page - */ - public async setPage(page: number): Promise { - const control = await this.pageControl; - if (control) { - control.page = page; - } - } - - /** - * resets internal pageControl's current page to 0 - */ - public async resetPage() { - await this.setPage(0); - } - - async firstUpdated() { - if (typeof ResizeObserver === 'function') { - const ro = new ResizeObserver((entries) => { - for (const entry of entries) { - const cr = entry.contentRect; - this.narrow = cr.width < this.narrowMaxWidth; - this.updateChildrenIsNarrow(); - } - }); - - ro.observe(this); - } else { - const mql = window.matchMedia('(max-width: 768px)'); - mql.addEventListener('change', (e) => { - this.narrow = e.matches; - this.updateChildrenIsNarrow(); - }); - this.narrow = mql.matches; - this.updateChildrenIsNarrow(); - } - - this.addEventListener(DataTableItemDropEvent.eventType, (e: DataTableItemDropEvent) => { - e.stopPropagation(); - //HoverIndex cannot be dropped beyond the length of the array - const hoverIndex = Math.min(e.hoverIndex, this.items.length - 1); - - //Ignore if item goes back to where it started - if (hoverIndex !== e.originIndex) { - const temp = this.items[e.originIndex]; - this.items.splice(e.originIndex, 1); - this.items.splice(hoverIndex, 0, temp); - /** - * @ignore - */ - this.dispatchEvent(new DataTableItemsReorderedEvent()); - } - }); - - //When slotted in items change, sync the narrow prop - this.tableHeaders.addEventListener('slotchange', () => this.updateChildrenIsNarrow()); - this.itemsSlot.addEventListener('slotchange', () => this.updateChildrenIsNarrow()); - - await ( - await this.pageControl - )?.updateComplete; - } - - protected updateChildrenIsNarrow() { - this.hasDragItems = (this.itemsSlot.assignedElements() as Array).some((o) => o.enableDrag); - (this.itemsSlot.assignedElements() as Array).forEach((o) => (o.narrow = this.narrow)); - (this.tableHeaders.assignedElements() as Array).forEach((o) => (o.narrow = this.narrow)); - } - - /** - * de-select all table items and clear this.selected - */ - clearSelection() { - this.#deselectAll(); - // Ensure the collection is empty, deselect can cause a race condition - // between deselecting and UI drawing new items. - - if (this.selected.length > 0) { - this.selected = []; - this.#notifySelectedChanged(); - } - } - - updated(changedProps) { - if (changedProps.has('items') && changedProps.get('items') !== this.items) { - // Clear selection when items array changes. - this.clearSelection(); - } - } - - #notifySelectedChanged() { - this.dispatchEvent(new CustomEvent('selected-changed', { composed: true, detail: this.selected })); - } - - /** - * display linear progress bar while promise is active - */ - async loadWhile(promise: Promise) { - this.isLoading = true; - this.#openCount++; - try { - await promise; - } finally { - this.#openCount--; - if (this.#openCount === 0) { - this.isLoading = false; - } - } - } - - #deselectAll() { - this.#getTableItems().forEach((o) => o.deselect()); - } - - /** - * select all table items - */ - selectAll() { - if (!this.singleSelect) { - this.#getTableItems().forEach((o) => o.select()); - } - } - - #getTableItems(): Array { - return (this.itemsSlot.assignedElements() as Array).filter( - (o) => typeof o.select === 'function' && typeof o.deselect === 'function' - ) as Array; - } - - static styles = [ - h1, - ellipsis, - niceBadgeStyles, - css` - :host { - display: flex; - flex-direction: column; - - border: 1px solid var(--md-sys-color-outline-variant); - background-color: var(--md-sys-color-surface); - color: var(--md-sys-color-on-surface); - border-radius: 8px; - font-family: var(--titanium-data-table-font-family, Roboto, Noto, sans-serif); - --titanium-page-control-font-family: var(--titanium-data-table-font-family, Roboto, Noto, sans-serif); - -webkit-font-smoothing: antialiased; - } - - header { - display: flex; - flex-direction: column; - padding-bottom: 12px; - gap: 12px; - border-bottom: 1px solid var(--md-sys-color-outline-variant); - position: relative; - } - - /* HEADER ROW ONE */ - - section[row-one] { - display: grid; - grid: 'head menu' / 1fr auto; - gap: 8px; - padding: 12px 12px 0 12px; - } - - section[row-one] div[head] { - grid-area: head; - } - - section[row-one] div[menu] { - grid-area: menu; - } - - div[search] { - grid-area: search; - } - - /* HEADER ROW TWO */ - - section[row-two] { - display: grid; - grid: 'search-filter add' / 1fr auto; - gap: 8px; - padding: 0 12px 0 20px; - } - - :host([narrow]) section[row-two] { - grid: - 'search-filter ' - 'add' / auto; - } - - section[row-two] div[search-filter] { - grid-area: search-filter; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - } - - section[row-two] div[add-button] { - grid-area: add; - justify-self: end; - align-self: end; - } - - h1 { - padding: 12px 12px 0 12px; - } - - selected-actions { - display: grid; - gap: 6px 24px; - grid: 'selected-text buttons'; - background-color: var(--md-sys-color-secondary-container); - position: absolute; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - margin: 0 !important; - padding: 0 12px 12px 24px; - align-content: end; - z-index: 1; - } - - selected-actions h2 { - color: var(--md-sys-color-on-secondary-container); - font-size: 18px; - font-weight: 400; - align-self: end; - } - - selected-actions div[buttons] { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0 8px; - justify-content: flex-end; - } - - table-header { - display: flex; - flex-direction: row; - gap: 16px; - min-height: 48px; - border-bottom: 1px solid var(--md-sys-color-outline-variant); - } - - table-header ::slotted(titanium-data-table-header:last-of-type) { - padding-right: 24px; - } - - :host([has-drag-items]) table-header ::slotted(titanium-data-table-header:last-of-type) { - padding-right: 40px; - } - - md-linear-progress { - width: 100%; - margin-top: -4px; - } - - main { - position: relative; - min-height: 48px; - } - - content-veil { - display: none; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: var(--md-sys-color-scrim); - opacity: 0; - -webkit-transition: opacity 75ms linear; - -o-transition: opacity 75ms linear; - transition: opacity 75ms linear; - z-index: 6; - } - - content-veil[opened] { - opacity: 0.12; - display: block; - } - - table-message { - display: flex; - place-items: center; - justify-content: center; - gap: 8px; - padding: 64px; - - font-size: 14px; - z-index: 10; - line-height: 20px; - border-bottom: 1px solid var(--md-sys-color-outline-variant); - } - - table-message md-icon { - align-self: center; - flex-shrink: 0; - } - - footer { - display: grid; - grid: 'controls footer-slot' / minmax(400px, 1fr) auto; - gap: 24px; - padding: 12px; - align-items: center; - margin-top: -1px; - border-top: 1px solid var(--md-sys-color-outline-variant); - } - - titanium-page-control { - grid-area: controls; - margin-left: 12px; - justify-self: start; - } - - div[footer] { - justify-self: end; - } - - :host([narrow]) footer { - grid: - 'controls' - 'footer-slot' / auto; - } - - :host([disable-paging]) footer { - grid: 'footer-slot' / auto; - } - - footer-buttons { - display: flex; - gap: 12px; - flex-wrap: wrap; - align-items: flex-end; - } - - div[add-button] { - display: flex; - align-items: center; - } - - div[items-slot] { - position: relative; - } - - md-checkbox { - flex-shrink: 0; - align-self: center; - margin: 0 14px 0 20px; - } - - :host([disable-select]) table-header ::slotted(titanium-data-table-header:first-of-type) { - padding-left: 24px; - } - - :host(:not([disable-select])[single-select]) table-header { - padding-left: 68px; - } - - [hidden] { - display: none !important; - } - `, - ]; - - render() { - return html` -
- -
-
-

${this.header}

-
-
- -
-
-
-
- - - -
-
- -
-
-
- - -

${this.selected.length} item${this.selected.length > 1 ? 's' : ''} selected

-
- -
-
-
- - { - e.stopPropagation(); - const dataTableItem = e.target as TitaniumDataTableItem; - - if (dataTableItem.selected) { - if (this.singleSelect) { - this.#getTableItems() - .filter((o) => o.item !== dataTableItem.item) - .forEach((o) => o.deselect()); - } - - this.selected.push(dataTableItem.item); - this.requestUpdate(); - this.#notifySelectedChanged(); - } else { - this.selected.splice(this.selected.indexOf(dataTableItem.item), 1); - this.requestUpdate(); - this.#notifySelectedChanged(); - } - }} - > - - ${this.disableSelect || this.singleSelect - ? '' - : html` - 0} - ?indeterminate=${this.selected.length !== 0 && this.selected.length !== this.items.length} - ?disabled=${this.items.length === 0} - @click=${() => { - if (this.selected.length > 0) { - this.#deselectAll(); - } else { - this.selectAll(); - } - this.checkbox.focus(); - }} - > - `} - - - - -
-
- -
- 0}> - info - ${this.searchTerm === '' || typeof this.searchTerm === 'undefined' || this.searchTerm === null - ? 'No results' - : `Your search of '${this.searchTerm}' did not match any results`} - Loading data... - -
-
-
- - ${this.disablePaging - ? '' - : html` - { - this.dispatchEvent(new CustomEvent('paging-changed', { composed: true })); - }} - > - `} -
- -
-
-
- `; - } -} - -export class DataTableItemsReorderedEvent extends Event { - static eventType = 'titanium-data-table-items-reorder'; - constructor() { - super(DataTableItemsReorderedEvent.eventType); - } -} diff --git a/packages/web/titanium/data-table/draggable-item-base.ts b/packages/web/titanium/data-table/draggable-item-base.ts index 2334579a8..5caf54fc1 100644 --- a/packages/web/titanium/data-table/draggable-item-base.ts +++ b/packages/web/titanium/data-table/draggable-item-base.ts @@ -18,14 +18,14 @@ export class ItemDropEvent extends Event { } export class DraggableItemBase extends LitElement { - @property({ type: Boolean, reflect: true, attribute: 'nudge-down' }) protected accessor nudgeDown: boolean; - @property({ type: Boolean, reflect: true, attribute: 'nudge-up' }) protected accessor nudgeUp: boolean; - @property({ type: Boolean, reflect: true, attribute: 'dragged' }) protected accessor dragged: boolean; - @property({ type: Boolean, reflect: true, attribute: 'dragging' }) protected accessor dragging: boolean; - @property({ type: Boolean, attribute: 'disable-drag', reflect: true }) protected accessor disableDrag: boolean; - @property({ type: Object }) protected accessor scrollableContainer: HTMLElement; - - @state() protected accessor nudgeHeight: number; + @property({ type: Boolean, reflect: true, attribute: 'nudge-down' }) protected accessor nudgeDown: boolean = false; + @property({ type: Boolean, reflect: true, attribute: 'nudge-up' }) protected accessor nudgeUp: boolean = false; + @property({ type: Boolean, reflect: true, attribute: 'dragged' }) protected accessor dragged: boolean = false; + @property({ type: Boolean, reflect: true, attribute: 'dragging' }) protected accessor dragging: boolean = false; + @property({ type: Boolean, attribute: 'disable-drag', reflect: true }) protected accessor disableDrag: boolean = false; + @property({ type: Object }) protected accessor scrollableContainer!: HTMLElement; + + @state() protected accessor nudgeHeight: number = 0; @state() protected accessor hoverIndex: number | null = null; @state() protected accessor originIndex: number | null = null; diff --git a/packages/web/titanium/data-table/filter-controller.ts b/packages/web/titanium/data-table/filter-controller.ts index 3e1615171..de7574b5e 100644 --- a/packages/web/titanium/data-table/filter-controller.ts +++ b/packages/web/titanium/data-table/filter-controller.ts @@ -110,7 +110,7 @@ export class FilterController { return this.#filters.get(key); } - #notifyTimer: number; + #notifyTimer!: number; #batchNotifyFiltersChanged() { clearTimeout(this.#notifyTimer); this.#setQueryString(); diff --git a/packages/web/titanium/data-table/page-control.ts b/packages/web/titanium/data-table/page-control.ts index 67476d31c..44d7a4050 100644 --- a/packages/web/titanium/data-table/page-control.ts +++ b/packages/web/titanium/data-table/page-control.ts @@ -1,11 +1,9 @@ -import { css, LitElement } from 'lit'; +import { css, html, LitElement } from 'lit'; import { property, customElement, queryAsync } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { MdOutlinedSelect } from '@material/web/select/outlined-select.js'; -import { literal, html } from 'lit/static-html.js'; +import { MdFilledSelect } from '@material/web/select/filled-select.js'; import '@material/web/iconbutton/icon-button'; -import '@material/web/select/outlined-select.js'; import '@material/web/select/filled-select.js'; import '@material/web/select/select-option.js'; @@ -38,12 +36,12 @@ export class TitaniumPageControl extends LitElement { /** * Total number of items in all pages. */ - @property({ type: Number }) accessor count: number; + @property({ type: Number }) accessor count: number = 0; /** * Local storage key to save the current page size. */ - @property({ type: String, attribute: 'local-storage-key' }) accessor localStorageKey: string; + @property({ type: String, attribute: 'local-storage-key' }) accessor localStorageKey: string = ''; /** * Label for the page control. If not provided, defaults to 'Items per page'. @@ -53,14 +51,9 @@ export class TitaniumPageControl extends LitElement { /** * Disables the page control select and page navigation buttons when true */ - @property({ type: Boolean }) accessor disabled: boolean; + @property({ type: Boolean }) accessor disabled: boolean = false; - /** - * Swaps out outlined select for a filled select. - */ - @property({ type: Boolean, attribute: 'filled' }) accessor filled: boolean = false; - - @queryAsync('md-select') protected accessor select: MdOutlinedSelect; + @queryAsync('md-select') protected accessor select!: MdFilledSelect; /** * Gets or sets take value and assigns it to local storage. @@ -141,13 +134,6 @@ export class TitaniumPageControl extends LitElement { display: flex; } - md-outlined-select { - min-width: 100px; - --md-outlined-field-top-space: 4px; - --md-outlined-field-bottom-space: 4px; - --md-outlined-select-text-field-container-shape: 8px; - } - md-filled-select { min-width: 100px; overflow: hidden; @@ -171,27 +157,26 @@ export class TitaniumPageControl extends LitElement { `; render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html` -
${this.label}
- <${this.filled ? literal`md-filled-select` : literal`md-outlined-select`} - ?disabled=${this.disabled} - @request-selection=${(e) => { - e.stopPropagation(); - this.take = Number(e.target.value); - this.dispatchEvent(new CustomEvent('action', { composed: true })); - }} - > - ${repeat( - this.pageSizes, - (o) => o, - (o) => - html` -
${o}
-
` - )} - +
${this.label}
+ { + e.stopPropagation(); + this.take = Number(e.target.value); + this.dispatchEvent(new CustomEvent('action', { composed: true })); + }} + > + ${repeat( + this.pageSizes, + (o) => o, + (o) => + html` +
${o}
+
` + )} +
${this.#getPageStats(this.page, this.count)} @@ -203,6 +188,5 @@ export class TitaniumPageControl extends LitElement {
`; - /* eslint-enable lit/binding-positions, lit/no-invalid-html */ } } diff --git a/packages/web/titanium/date-input/date-input.ts b/packages/web/titanium/date-input/date-input.ts index dc58f0806..b0b3c25ac 100644 --- a/packages/web/titanium/date-input/date-input.ts +++ b/packages/web/titanium/date-input/date-input.ts @@ -5,12 +5,11 @@ import { redispatchEvent } from '@material/web/internal/events/redispatch-event' import { stringConverter } from '@material/web/internal/controller/string-converter'; -import '@material/web/field/outlined-field'; import '@material/web/field/filled-field'; import '@material/web/icon/icon'; import '@material/web/iconbutton/icon-button'; import { Field } from '@material/web/field/internal/field'; -import { html, literal } from 'lit/static-html.js'; +import { html } from 'lit'; /** * A date input the works in Firefox, Safari and Chrome @@ -88,11 +87,6 @@ export class TitaniumDateInput extends LitElement { @property() accessor label = ''; - /** - * Swaps out outlined text field for a filled text field. - */ - @property({ type: Boolean, attribute: 'filled' }) accessor filled: boolean = false; - @property({ type: Boolean, reflect: true }) accessor required = false; /** @@ -175,7 +169,7 @@ export class TitaniumDateInput extends LitElement { */ @property({ reflect: true, type: String }) accessor autocomplete = ''; - @query('input') private accessor input: HTMLInputElement; + @query('input') private accessor input!: HTMLInputElement; @queryAssignedElements({ slot: 'leading-icon' }) private accessor leadingIcons!: Element[]; @queryAssignedElements({ slot: 'trailing-icon' }) private accessor trailingIcons!: Element[]; @@ -262,7 +256,7 @@ export class TitaniumDateInput extends LitElement { this.nativeErrorText = this.validationMessage; if (prevMessage === this.getErrorText()) { - (this.shadowRoot?.querySelector(this.filled ? 'md-filled-field' : 'md-outlined-field') as Field)?.reannounceError(); + (this.shadowRoot?.querySelector('md-filled-field') as Field)?.reannounceError(); } return valid; @@ -295,7 +289,7 @@ export class TitaniumDateInput extends LitElement { display: block; } - :host([filled]) { + :host { --md-filled-field-container-shape: 16px; --md-filled-field-active-indicator-height: 0; @@ -307,12 +301,6 @@ export class TitaniumDateInput extends LitElement { --md-filled-field-label-text-populated-line-height: 14px; } - :host(:not([filled])) { - --md-outlined-field-top-space: 15px; - --md-outlined-field-bottom-space: 15px; - } - - md-outlined-field, md-filled-field { width: 100%; } @@ -329,14 +317,7 @@ export class TitaniumDateInput extends LitElement { /* Safari Only */ _::-webkit-full-page-media, _:future, - input { - padding-top: 14px; - padding-bottom: 7px; - } - - :host([filled]) _::-webkit-full-page-media, - :host([filled]) _:future, - :host([filled]) input { + :host input { padding-top: 21px; padding-bottom: 0; } @@ -355,14 +336,9 @@ export class TitaniumDateInput extends LitElement { input { min-width: 100px; - padding-bottom: 10px; - padding-top: 16px; - height: 30px; - } - - :host([filled]) input { padding-bottom: 3px; padding-top: 23px; + height: 30px; } } @@ -380,21 +356,15 @@ export class TitaniumDateInput extends LitElement { display: none; } - :host([filled]) { + :host { --md-filled-field-label-text-populated-line-height: 16px; } - - :host(:not([filled])) { - --md-outlined-field-top-space: 16px; - --md-outlined-field-bottom-space: 16px; - } } `; protected render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html` - <${this.filled ? literal`md-filled-field` : literal`md-outlined-field`} + - + `; - /* eslint-enable lit/binding-positions, lit/no-invalid-html */ } } diff --git a/packages/web/titanium/date-range-selector/date-range-selector.ts b/packages/web/titanium/date-range-selector/date-range-selector.ts index f0ae07f9f..97e6f5015 100644 --- a/packages/web/titanium/date-range-selector/date-range-selector.ts +++ b/packages/web/titanium/date-range-selector/date-range-selector.ts @@ -1,5 +1,4 @@ import '@material/web/icon/icon'; -import '@material/web/field/outlined-field'; import '@material/web/field/filled-field'; import '@material/web/menu/menu'; import '@material/web/button/text-button'; @@ -10,7 +9,7 @@ import '@material/web/list/list-item'; import '../date-input/date-input'; import { css, LitElement, nothing } from 'lit'; -import { html, literal } from 'lit/static-html.js'; +import { html } from 'lit'; import { property, customElement, query, state } from 'lit/decorators.js'; import dayjs from 'dayjs/esm'; import { DateRangeOption } from './types/date-range-option'; @@ -63,11 +62,6 @@ export class TitaniumDateRangeSelector extends LitElement { */ @property({ type: Boolean, reflect: true }) accessor disabled: boolean = false; - /** - * Whether or not the input should be filled - */ - @property({ type: Boolean, reflect: true }) accessor filled: boolean = false; - /** * Override default ranges with custom options. Needs to contain, at least, 'allTime'. */ @@ -87,7 +81,7 @@ export class TitaniumDateRangeSelector extends LitElement { @state() private accessor proposedRange: string = 'custom'; @state() private accessor proposedStartDate: string = ''; @state() private accessor proposedEndDate: string = ''; - @state() protected accessor open: boolean; + @state() protected accessor open: boolean = false; @state() private accessor focused = false; async updated(changedProps) { @@ -130,7 +124,7 @@ export class TitaniumDateRangeSelector extends LitElement { position: relative; } - :host([filled]) { + :host { --md-menu-container-shape: 16px; --md-filled-field-container-shape: 16px; @@ -174,10 +168,6 @@ export class TitaniumDateRangeSelector extends LitElement { gap: 12px; padding: 12px; - border-radius: 0 0 4px 4px; - } - - :host([filled]) menu-actions { border-radius: 0 0 16px 16px; } @@ -191,13 +181,11 @@ export class TitaniumDateRangeSelector extends LitElement { margin-right: 16px; } - md-filled-field, - md-outlined-field { + md-filled-field { width: 100%; } - md-filled-field md-icon, - md-outlined-field md-icon { + md-filled-field md-icon { margin: 0 12px; } @@ -274,9 +262,8 @@ export class TitaniumDateRangeSelector extends LitElement { } render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html` - <${this.filled ? literal`md-filled-field` : literal`md-outlined-field`} + ${this.#getRange(this.range)?.icon || 'date_range'} ${this.open ? 'arrow_drop_up' : 'arrow_drop_down'} - + - ${ - this.open - ? Array.from(this.customDateRanges ? this.customDateRanges : DateRanges).map( - (o) => - html` { - this.proposedRange = o[0]; - const range = this.#getRange(o[0]); - if (range) { - this.proposedStartDate = range.startDate() ?? ''; - this.proposedEndDate = range.endDate() ?? ''; - } - }} - value=${o[0]} - > - ${o[1].icon} -
${o[1].name}
-
` - ) - : nothing - } + ${this.open + ? Array.from(this.customDateRanges ? this.customDateRanges : DateRanges).map( + (o) => + html` { + this.proposedRange = o[0]; + const range = this.#getRange(o[0]); + if (range) { + this.proposedStartDate = range.startDate() ?? ''; + this.proposedEndDate = range.endDate() ?? ''; + } + }} + value=${o[0]} + > + ${o[1].icon} +
${o[1].name}
+
` + ) + : nothing} (this.proposedRange = 'custom')} value="custom"> date_range
Custom range
@@ -372,7 +357,6 @@ export class TitaniumDateRangeSelector extends LitElement { start-date label="From" type=${this.type} - .filled=${this.filled} .value=${this.proposedStartDate ?? ''} @change=${async (e: DOMEvent) => { this.proposedStartDate = e.target.value ?? ''; @@ -389,7 +373,6 @@ export class TitaniumDateRangeSelector extends LitElement { end-date label="To" type=${this.type} - .filled=${this.filled} .value=${this.proposedEndDate ?? ''} @change=${async (e: DOMEvent) => { this.proposedEndDate = e.target.value ?? ''; @@ -409,10 +392,8 @@ export class TitaniumDateRangeSelector extends LitElement { (this.open = false)}>Cancel { if (!this.#validateDates(this.proposedStartDate, this.proposedEndDate)) { return; @@ -429,6 +410,5 @@ export class TitaniumDateRangeSelector extends LitElement {
`; - /* eslint-enable lit/binding-positions, lit/no-invalid-html */ } } diff --git a/packages/web/titanium/drawer/drawer.ts b/packages/web/titanium/drawer/drawer.ts index c76413c5e..7f45afc99 100644 --- a/packages/web/titanium/drawer/drawer.ts +++ b/packages/web/titanium/drawer/drawer.ts @@ -13,19 +13,13 @@ import { redispatchEvent } from '@material/web/internal/events/redispatch-event' */ @customElement('titanium-drawer') export class TitaniumDrawer extends LitElement { - @query('dialog') private accessor dialog: HTMLDialogElement | null; + @query('dialog') private accessor dialog!: HTMLDialogElement | null; /** - * Set the position of content fixed when menu is closed. Only takes effect if always-show-content is set. + * Set the position of content fixed when menu is closed. Only takes effect in inline mode. */ @property({ type: Boolean, reflect: true }) accessor fixed: boolean = false; - /** - * Show the slotted content regardless if the menu is open or closed - * @deprecated use mode instead - */ - @property({ type: Boolean, reflect: true, attribute: 'always-show-content' }) accessor alwayShowContent: boolean = false; - /** * Reverse the direction of the drawer opening and closing animations */ @@ -35,7 +29,7 @@ export class TitaniumDrawer extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'has-footer' }) private accessor hasFooter = false; @property({ type: Boolean, reflect: true, attribute: 'keep-open-when-going-to-flyover' }) private accessor keepOpenWhenGoingToFlyover = false; - @property({ type: String, reflect: true, attribute: 'mode' }) accessor mode: 'inline' | 'flyover' | null; + @property({ type: String, reflect: true, attribute: 'mode' }) accessor mode: 'inline' | 'flyover' | null = null; @property({ type: Boolean, reflect: true, attribute: 'open' }) accessor isOpen: boolean = false; //read only @queryAssignedElements({ slot: 'header' }) private readonly headerElements!: Element[]; @@ -44,11 +38,6 @@ export class TitaniumDrawer extends LitElement { async updated(changedProps: PropertyValues) { if (changedProps.has('mode')) { if (this.mode === 'inline') { - if (this.isOpen) { - //drawer was already open in inline mode, so we need to keep it open - this.alwayShowContent = true; - } - //close the flyover drawer, we are inline this.dialog?.close(); } @@ -64,9 +53,6 @@ export class TitaniumDrawer extends LitElement { this.#setOpen(false); } } - - //we are now in flyover mode, we need to hide the inline content - this.alwayShowContent = false; } } } @@ -130,7 +116,6 @@ export class TitaniumDrawer extends LitElement { open() { if (this.mode === 'inline') { this.#setOpen(true); - this.alwayShowContent = true; } else { this.dialog?.showModal(); this.dialog?.removeAttribute('hide'); @@ -144,7 +129,6 @@ export class TitaniumDrawer extends LitElement { async close() { if (this.mode === 'inline') { this.#setOpen(false); - this.alwayShowContent = false; } else { this.dialog?.setAttribute('hide', ''); await TitaniumDrawer.animationsComplete(this.dialog!); @@ -167,7 +151,6 @@ export class TitaniumDrawer extends LitElement { closeQuick() { if (this.mode === 'inline') { this.#setOpen(false); - this.alwayShowContent = false; } else { this.dialog?.close(); } @@ -281,7 +264,7 @@ export class TitaniumDrawer extends LitElement { opacity: 0.8; } - :host([always-show-content]) dialog:not([open]) { + :host([mode='inline'][open]) dialog:not([open]) { position: sticky; top: var(--titanium-drawer-full-height-padding, 48px); @@ -299,7 +282,7 @@ export class TitaniumDrawer extends LitElement { animation: show 0.25s ease normal; } - :host([always-show-content][direction='rtl']) dialog:not([open]) { + :host([mode='inline'][open][direction='rtl']) dialog:not([open]) { animation: show-reverse 0.25s ease normal; } diff --git a/packages/web/titanium/duration-input/duration-input.ts b/packages/web/titanium/duration-input/duration-input.ts deleted file mode 100644 index a3c11c23d..000000000 --- a/packages/web/titanium/duration-input/duration-input.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { property, customElement } from 'lit/decorators.js'; -import { PropertyValues, css } from 'lit'; - -import dayjs from 'dayjs/esm'; -import duration from 'dayjs/esm/plugin/duration'; -import { ExtendableOutlinedTextField } from '../extendable-outlined-text-field/extendable-outlined-text-field'; -import humanInterval, { durationToString } from './human-interval'; -dayjs.extend(duration); - -/** - * titanium-duration-input is a human readable duration textfield. - * - * @element titanium-duration-input - * - * @fires duration-change The duration can be accessed via event.target.duration - * - */ - -@customElement('titanium-duration-input') -export class TitaniumDurationInput extends ExtendableOutlinedTextField { - /** - * Dayjs duration object. This is the main property you will interact with because the value - * property of this component is actually the human readable string and not the duration you most likely - * want to work with. When changed a duration-change event will be dispatched. - */ - @property({ type: Object }) accessor duration: duration.Duration | null = null; - - @property({ reflect: true, type: String }) accessor autocomplete: string = 'off'; - - @property({ reflect: true, type: Boolean }) accessor spellcheck: boolean = false; - - @property({ reflect: true, type: String }) accessor placeholder: string = '3 hours and 30 minutes'; - @property({ reflect: true, type: String }) accessor label: string = 'Duration'; - - firstUpdated() { - this.addEventListener('change', () => { - this.#customReportValidity(this.input.value); - - const dur = this.#textToInterval(this.input.value); - if (dur?.asMilliseconds() != this.duration?.asMilliseconds()) { - this.duration = dur; - this.dispatchEvent(new Event('duration-change')); - } - }); - } - - static styles = css` - :host { - display: block; - } - - md-outlined-text-field { - width: 100%; - } - `; - - updated(changedProps: PropertyValues) { - if (changedProps.has('duration') && changedProps.get('duration') !== this.duration) { - if (this.duration) { - this.value = durationToString(this.duration); - } else { - this.duration = null; - this.value = ''; - } - } - } - - checkValidity() { - return super.checkValidity() && this.#customCheckValidity(this.input.value); - } - - reportValidity() { - this.#customReportValidity(this.input.value); - return super.reportValidity(); - } - - #customCheckValidity(input: string) { - if (input && !this.#textToInterval(input)) { - return false; - } else { - return true; - } - } - - #customReportValidity(input: string) { - if (!this.#customCheckValidity(input)) { - this.error = true; - this.errorText = 'Duration was entered in an incorrect format. Try "3 hours and 30 minutes"'; - } else { - this.error = false; - this.errorText = ''; - } - } - - #textToInterval(input: string) { - if (!input) { - return null; - } - const ms = humanInterval(input); - return isNaN(ms) ? null : dayjs.duration(ms, 'ms'); - } - - override async reset() { - super.reset(); - this.error = false; - this.errorText = ''; - this.duration = null; - } -} diff --git a/packages/web/titanium/duration-input/outlined-duration-input.ts b/packages/web/titanium/duration-input/outlined-duration-input.ts deleted file mode 100644 index da74b2ff8..000000000 --- a/packages/web/titanium/duration-input/outlined-duration-input.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { property, customElement } from 'lit/decorators.js'; -import { PropertyValues } from 'lit'; -import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; - -import dayjs from 'dayjs/esm'; -import duration from 'dayjs/esm/plugin/duration'; -import humanInterval, { durationToString } from './human-interval'; -dayjs.extend(duration); - -/** - * titanium-outlined-duration-input is a human readable duration textfield. - * - * @element titanium-outlined-duration-input - * - * @fires duration-change The duration can be accessed via event.target.duration - * - */ - -@customElement('titanium-outlined-duration-input') -export class TitaniumOutlinedDurationInput extends MdOutlinedTextField { - /** - * Dayjs duration object. This is the main property you will interact with because the value - * property of this component is actually the human readable string and not the duration you most likely - * want to work with. When changed a duration-change event will be dispatched. - */ - @property({ type: Object }) accessor duration: duration.Duration | null = null; - @property({ type: String }) label: string = 'Duration'; - @property({ type: String }) supportingText: string = 'Enter a duration ex. "3 hours and 30 minutes"'; - @property({ type: String }) autocomplete: string = 'off'; - @property({ type: Boolean }) spellcheck: boolean = false; - - updated(changedProps: PropertyValues) { - if (changedProps.has('duration') && changedProps.get('duration') !== this.duration) { - if (this.duration) { - this.value = durationToString(this.duration); - } else { - this.duration = null; - this.value = ''; - } - } - } - - firstUpdated() { - this.addEventListener('change', () => { - this.reportValidity(); - - const dur = this.#textToInterval(this.value); - if (dur?.asMilliseconds() != this.duration?.asMilliseconds()) { - this.duration = dur; - this.dispatchEvent(new Event('duration-change')); - } - }); - } - - checkValidity() { - return super.checkValidity() && this.#customCheckValidity(this.value); - } - - reportValidity() { - if (!this.#customCheckValidity(this.value)) { - this.error = true; - this.errorText = 'Duration was entered in an incorrect format. Try "3 hours and 30 minutes"'; - } else { - this.error = false; - this.errorText = ''; - } - return super.reportValidity(); - } - - #customCheckValidity(input: string) { - if (input && !this.#textToInterval(input)) { - return false; - } else { - return true; - } - } - - #textToInterval(input: string) { - if (!input) { - return null; - } - const ms = humanInterval(input); - return isNaN(ms) ? null : dayjs.duration(ms, 'ms'); - } - - override async reset() { - super.reset(); - this.error = false; - this.errorText = ''; - this.duration = null; - } -} diff --git a/packages/web/titanium/error-page/error-page.ts b/packages/web/titanium/error-page/error-page.ts deleted file mode 100644 index 6319191ce..000000000 --- a/packages/web/titanium/error-page/error-page.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { css, html, LitElement, TemplateResult } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; - -/** - * A pre-styled error page - * - * @element titanium-error-page - * - */ - -@customElement('titanium-error-page') -export class TitaniumErrorPage extends LitElement { - /** - * Reason text for the error - */ - @property() accessor message: string | TemplateResult<1> = 'We were unable to find the page you are looking for...'; - - static styles = css` - :host { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - - font-family: Roboto, sans-serif; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - - max-width: 1300px; - } - - header { - flex: 1 1 auto; - margin-right: 24px; - } - - h1 { - font-family: Metropolis, 'Roboto', 'Noto', sans-serif; - font-weight: 600; - font-size: 75px; - line-height: 85px; - margin: 0; - } - - h2 { - font-weight: 400; - margin: 8px 0 0 4px; - max-width: 75%; - } - - img { - flex-shrink: 0; - height: 280px; - width: 280px; - } - - @media (max-width: 768px) { - :host { - margin-top: 24px; - } - - h2 { - max-width: inherit; - font-size: 21px; - } - - img { - height: 120px; - width: 120px; - align-self: flex-start; - } - - h1 { - font-size: 55px; - line-height: 65px; - } - } - `; - - render() { - return html` -
-

Oops!

-

${this.message}

-
- - `; - } -} diff --git a/packages/web/titanium/event-bus/event-bus.ts b/packages/web/titanium/event-bus/event-bus.ts index 1cf6588ad..3843be3bd 100644 --- a/packages/web/titanium/event-bus/event-bus.ts +++ b/packages/web/titanium/event-bus/event-bus.ts @@ -18,7 +18,7 @@ export class EventBus { if (Array.isArray(eventTypes)) { eventTypes.forEach((o) => this.subscribe(entityType, o, callback)); } else { - this.#addSubscription(entityType, eventTypes, callback); + this.#addSubscription(entityType, eventTypes, callback as EventCallback); } } } diff --git a/packages/web/titanium/extendable-outlined-text-field/extendable-outlined-text-field.ts b/packages/web/titanium/extendable-outlined-text-field/extendable-outlined-text-field.ts deleted file mode 100644 index 31564dd9b..000000000 --- a/packages/web/titanium/extendable-outlined-text-field/extendable-outlined-text-field.ts +++ /dev/null @@ -1,277 +0,0 @@ -import '@material/web/textfield/outlined-text-field'; - -import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; -import { LitElement, html } from 'lit'; -import { property, query } from 'lit/decorators.js'; -import { stringConverter } from '@material/web/internal/controller/string-converter'; -import { TextFieldType, UnsupportedTextFieldType } from '@material/web/textfield/internal/text-field'; -import { DOMEvent } from '../types/dom-event'; -import { redispatchEvent } from '@material/web/internal/events/redispatch-event'; - -export class ExtendableOutlinedTextField extends LitElement { - @query('md-outlined-text-field') accessor input: MdOutlinedTextField; - - /** - * Gets or sets whether or not the text field is in a visually invalid state. - * - * This error state overrides the error state controlled by - * `reportValidity()`. - */ - @property({ type: Boolean, reflect: true }) accessor error = false; - - @property({ type: Boolean, reflect: true }) accessor disabled = false; - - /** - * The error message that replaces supporting text when `error` is true. If - * `errorText` is an empty string, then the supporting text will continue to - * show. - * - * This error message overrides the error message displayed by - * `reportValidity()`. - */ - @property({ attribute: 'error-text' }) accessor errorText = ''; - - @property() accessor label = ''; - - @property({ type: Boolean, reflect: true }) accessor required = false; - - /** - * The current value of the text field. It is always a string. - */ - @property() accessor value = ''; - - /** - * An optional prefix to display before the input value. - */ - @property({ attribute: 'prefix-text' }) accessor prefixText = ''; - - /** - * An optional suffix to display after the input value. - */ - @property({ attribute: 'suffix-text' }) accessor suffixText = ''; - - /** - * Whether or not the text field has a leading icon. Used for SSR. - */ - @property({ type: Boolean, attribute: 'has-leading-icon' }) - accessor hasLeadingIcon = false; - - /** - * Whether or not the text field has a trailing icon. Used for SSR. - */ - @property({ type: Boolean, attribute: 'has-trailing-icon' }) - accessor hasTrailingIcon = false; - - /** - * Conveys additional information below the text field, such as how it should - * be used. - */ - @property({ attribute: 'supporting-text' }) accessor supportingText = ''; - - /** - * Override the input text CSS `direction`. Useful for RTL languages that use - * LTR notation for fractions. - */ - @property({ attribute: 'text-direction' }) accessor textDirection = ''; - - /** - * The number of rows to display for a `type="textarea"` text field. - * Defaults to 2. - */ - @property({ type: Number }) accessor rows = 2; - - /** - * The number of cols to display for a `type="textarea"` text field. - * Defaults to 20. - */ - @property({ type: Number }) accessor cols = 20; - - // properties - @property({ reflect: true }) override accessor inputMode = ''; - - /** - * Defines the greatest value in the range of permitted values. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#max - */ - @property() accessor max = ''; - - /** - * The maximum number of characters a user can enter into the text field. Set - * to -1 for none. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#maxlength - */ - @property({ type: Number }) accessor maxLength = -1; - - /** - * Defines the most negative value in the range of permitted values. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#min - */ - @property() accessor min = ''; - - /** - * The minimum number of characters a user can enter into the text field. Set - * to -1 for none. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#minlength - */ - @property({ type: Number }) accessor minLength = -1; - - /** - * A regular expression that the text field's value must match to pass - * constraint validation. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#pattern - */ - @property() accessor pattern = ''; - - @property({ reflect: true, converter: stringConverter }) accessor placeholder = ''; - - /** - * Indicates whether or not a user should be able to edit the text field's - * value. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#readonly - */ - @property({ type: Boolean, reflect: true }) accessor readOnly = false; - - /** - * Indicates that input accepts multiple email addresses. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#multiple - */ - @property({ type: Boolean, reflect: true }) accessor multiple = false; - - /** - * Returns or sets the element's step attribute, which works with min and max - * to limit the increments at which a numeric or date-time value can be set. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#step - */ - @property() accessor step = ''; - - /** - * The `` type to use, defaults to "text". The type greatly changes how - * the text field behaves. - * - * Text fields support a limited number of `` types: - * - * - text - * - textarea - * - email - * - number - * - password - * - search - * - tel - * - url - * - * See - * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types - * for more details on each input type. - */ - @property({ reflect: true }) accessor type: TextFieldType | UnsupportedTextFieldType = 'text'; - - /** - * Describes what, if any, type of autocomplete functionality the input - * should provide. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete - */ - @property({ reflect: true, type: String }) accessor autocomplete = ''; - - /** - * Describes what, if any, type of spellcheck functionality the input - * should provide. - * - * https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/spellcheck - */ - @property({ reflect: true, type: Boolean }) accessor spellcheck; - - checkValidity() { - return this.input.checkValidity(); - } - - reportValidity() { - return this.input.reportValidity(); - } - - select() { - this.input.select(); - } - - setCustomValidity(error: string) { - this.input.setCustomValidity(error); - } - - setRangeText(replacement: string, start: number, end: number, selectionMode?: SelectionMode) { - this.input.setRangeText(replacement, start, end, selectionMode); - } - - setSelectionRange(start: number | null, end: number | null, direction?: 'forward' | 'backward' | 'none') { - this.input.setSelectionRange(start, end, direction); - } - - stepDown(stepDecrement?: number) { - this.input.stepDown(stepDecrement); - } - - stepUp(stepDecrement?: number) { - this.input.stepUp(stepDecrement); - } - - reset() { - this.input.reset(); - } - - focus() { - this.input.focus(); - } - - protected renderMainSlot() { - return html` - - - - `; - } - - render() { - return html` - ) => (this.value = e.target.value)} - @blur=${(e) => redispatchEvent(this, e)} - @focus=${(e) => redispatchEvent(this, e)} - @change=${(e) => redispatchEvent(this, e)} - @invalid=${(e) => redispatchEvent(this, e)} - .disabled=${this.disabled} - .required=${this.required} - .error=${this.error} - .autocomplete=${this.autocomplete} - .spellcheck=${this.spellcheck} - .errorText=${this.errorText} - .hasLeadingIcon=${this.hasLeadingIcon} - .hasTrailingIcon=${this.hasTrailingIcon} - .label=${this.label} - .max=${this.max} - .maxLength=${this.maxLength} - .minLength=${this.minLength} - .pattern=${this.pattern} - .placeholder=${this.placeholder} - .prefixText=${this.prefixText} - .readOnly=${this.readOnly} - .rows=${this.rows} - .step=${this.step} - .suffixText=${this.suffixText} - .supportingText=${this.supportingText} - .textDirection=${this.textDirection} - .type=${this.type} - .value=${this.value} - > - ${this.renderMainSlot()} - - `; - } -} diff --git a/packages/web/titanium/full-page-loading-indicator/full-page-loading-indicator.ts b/packages/web/titanium/full-page-loading-indicator/full-page-loading-indicator.ts deleted file mode 100644 index 5d3622e92..000000000 --- a/packages/web/titanium/full-page-loading-indicator/full-page-loading-indicator.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { css, html, LitElement } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { PendingStateEvent } from '../types/pending-state-event'; - -import '@material/web/progress/linear-progress'; - -/** - * A simple full-screen veil with loading indicator that uses promise driven pending-state-events - * - * @element titanium-full-page-loading-indicator - * - * - */ -@customElement('titanium-full-page-loading-indicator') -export class TitaniumFullPageLoadingIndicator extends LitElement { - @state() private accessor open: boolean; - - #openDelayTimer: number; - #closeDelayTimer: number; - - //Promises faster than this do not cause the scrim to open at all - //Prevents flicker for fast promises - #openDelay: number = 75; - - // min time scrim has to remain open - #minTimeOpen: number = 350; - #timeOpen: number; - #openCount = 0; - - firstUpdated() { - this.popover = 'manual'; - this.addEventListener('toggle', (e: ToggleEvent) => (this.open = e.newState === 'open')); - - window.addEventListener(PendingStateEvent.eventType, async (e: PendingStateEvent) => { - this.#open(); - this.#openCount++; - try { - await e.detail.promise; - } catch { - // Do nothing, this will be handled by others - } finally { - this.#openCount--; - if (this.#openCount === 0) { - this.#close(); - } - } - }); - } - - #open() { - window.clearTimeout(this.#openDelayTimer); - - //If re-opened while close timer is running, prevent the close - window.clearTimeout(this.#closeDelayTimer); - - this.#openDelayTimer = window.setTimeout(() => { - this.#timeOpen = performance.now(); - if (this.showPopover) { - this.showPopover(); - } else { - this.open = true; - } - this.style.display = 'block'; - }, this.#openDelay); - } - - #close() { - window.clearTimeout(this.#openDelayTimer); - const totalTimeOpened = performance.now() - this.#timeOpen; - const closeDelay = Math.max(this.#minTimeOpen - totalTimeOpened, 0); - - this.#closeDelayTimer = window.setTimeout(() => { - if (this.hidePopover) { - this.hidePopover(); - } else { - this.open = false; - } - this.style.display = 'none'; - }, closeDelay); - } - - static styles = css` - :host { - width: 100%; - height: 100%; - max-width: 100vw; - max-height: 100vh; - - border: 0; - inset: unset; - top: 0; - right: 0; - left: 0; - bottom: 0; - margin: 0; - padding: 0; - background: transparent; - } - - :host::backdrop { - background-color: var(--md-sys-color-scrim, #000); - backdrop-filter: blur(6px); - } - - :host(:popover-open)::backdrop { - opacity: 0.32; - } - - md-linear-progress { - position: absolute; - width: 100%; - top: 0; - right: 0; - left: 0; - } - `; - - render() { - return html` `; - } -} diff --git a/packages/web/titanium/header/header.ts b/packages/web/titanium/header/header.ts deleted file mode 100644 index a025f802b..000000000 --- a/packages/web/titanium/header/header.ts +++ /dev/null @@ -1,145 +0,0 @@ -import '@material/web/icon/icon'; -import '@material/web/iconbutton/icon-button'; - -import { h1, h3 } from '../../titanium/styles/styles'; -import { css, html, LitElement, nothing } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; - -/** - * A pre-styled page header with sub header and optional back button. - * - * @element titanium-header - * - * @fires titanium-header-back-click - Fired when the back button is clicked - * - * @cssprop {Color} [--md-sys-color-on-background] - Header text color - * @cssprop {Color} [--md-sys-color-on-surface-variant] - Sub-header text color - */ -@customElement('titanium-header') -export class TitaniumHeader extends LitElement { - /** - * Header text - */ - @property({ type: String }) accessor header: string; - - /** - * Sub-header text - */ - @property({ type: String }) accessor subHeader: string; - - /** - * Leading header icon - */ - @property({ type: String }) accessor icon: string; - - /** - * Removes the back button - */ - @property({ type: Boolean, reflect: true, attribute: 'no-nav' }) accessor noNav: boolean = false; - - /** - * Lets user override back button behavior - */ - @property({ type: Boolean, reflect: true, attribute: 'disable-default-back-button-behavior' }) accessor disableDefaultBackButtonBehavior: boolean = false; - - #handleBackClick() { - if (this.disableDefaultBackButtonBehavior) { - this.dispatchEvent(new CustomEvent('titanium-header-back-click', { composed: true })); - } else { - window.history.back(); - } - } - - static styles = [ - h1, - h3, - css` - :host { - display: flex; - flex-direction: column; - -webkit-font-smoothing: antialiased; - padding: 0 52px 8px 52px; - position: relative; - } - - :host([no-nav]) md-icon-button { - display: none; - } - - :host([no-nav]) { - padding: 0 0 8px 0; - } - - header { - display: block; - text-align: center; - padding: 0 0 8px 0; - } - - h1 { - display: inline; - font-size: 40px; - line-height: 42px; - font-weight: 200; - - margin: 0; - color: var(--md-sys-color-on-background); - } - - h3 { - color: var(--md-sys-color-on-surface-variant); - font-family: Metropolis, Roboto, Noto, sans-serif; - font-weight: 300; - font-size: 16px; - line-height: 20px; - text-align: center; - } - - md-icon[header-icon] { - display: inline; - vertical-align: text-bottom; - font-size: 40px; - color: var(--md-sys-color-on-background); - margin-right: 8px; - } - - md-icon-button { - position: absolute; - top: 0; - left: 0; - } - - @media (max-width: 920px) { - h1 { - font-size: 30px; - line-height: 32px; - } - - h3 { - font-size: 14px; - line-height: 16px; - } - - md-icon[header-icon] { - font-size: 30px; - } - } - - :host([hidden]) { - display: none; - } - `, - ]; - - render() { - return html` -
- ${this.icon ? html`${this.icon}` : nothing} -

${this.header}

- arrow_back -
- -

${this.subHeader}

- `; - } -} diff --git a/packages/web/titanium/helpers/debouncer.ts b/packages/web/titanium/helpers/debouncer.ts index f83f4dcbe..cd000ffb8 100644 --- a/packages/web/titanium/helpers/debouncer.ts +++ b/packages/web/titanium/helpers/debouncer.ts @@ -1,6 +1,6 @@ export class Debouncer { #debouncePromise: Promise | undefined; - #debounceResolve: { (arg0: T): void; (value?: T | PromiseLike | undefined): void }; + #debounceResolve!: (value: T) => void; #timer: number = 0; #work: (...args: A[]) => Promise; #interval: number; diff --git a/packages/web/titanium/helpers/helpers.ts b/packages/web/titanium/helpers/helpers.ts index 1c45c489a..3d0d8d6dd 100644 --- a/packages/web/titanium/helpers/helpers.ts +++ b/packages/web/titanium/helpers/helpers.ts @@ -5,7 +5,6 @@ export { escapeTerm } from './escape-term'; export { installMediaQueryWatcher } from './install-media-query-watcher'; export { getSearchTokens } from './get-search-token'; export { Debouncer } from './debouncer'; -export { LoadWhile } from './load-while'; export { promiseTracking } from './promise-tracking'; export { join } from './join'; export { delay } from './delay'; diff --git a/packages/web/titanium/helpers/load-while.ts b/packages/web/titanium/helpers/load-while.ts deleted file mode 100644 index 7b76197d4..000000000 --- a/packages/web/titanium/helpers/load-while.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type Constructor = { new (...args: any[]): T }; -export const LoadWhile = >(base: C) => - class extends base { - static get properties() { - return { - isLoading: { type: Boolean }, - }; - } - /** - * @internal - */ - #promiseCount = 0; - isLoading: boolean; - async loadWhile(promise: Promise) { - this.isLoading = true; - this.#promiseCount++; - try { - await promise; - } catch { - // Do nothing, this will be handled by others - } finally { - this.#promiseCount--; - if (this.#promiseCount === 0) { - this.isLoading = false; - } - } - } - }; diff --git a/packages/web/titanium/helpers/pending-state-catcher.ts b/packages/web/titanium/helpers/pending-state-catcher.ts index 455e2d42c..6579ee8f8 100644 --- a/packages/web/titanium/helpers/pending-state-catcher.ts +++ b/packages/web/titanium/helpers/pending-state-catcher.ts @@ -8,8 +8,16 @@ declare global { } export type Constructor = { new (...args: any[]): T }; -export const PendingStateCatcher = >(base: C) => - class extends base { + +export type PendingStateCatcherMixin = { + stateIsPending: boolean; + pendingStateCatcherLoadingStartDelay: number; + pendingStateCatcherMinTimeOpen: number; + pendingStateCatcherTarget: Promise | null; +}; + +export function PendingStateCatcher>(base: C): C & Constructor { + class PendingStateCatcherClass extends base { static get properties() { return { stateIsPending: { type: Boolean }, @@ -22,7 +30,7 @@ export const PendingStateCatcher = >(base: C) /** * !! Handled by the pending state catcher !! */ - public accessor stateIsPending: boolean; + public accessor stateIsPending!: boolean; /** * Promises faster than this do not cause stateIsPending to be set to true at all @@ -39,64 +47,68 @@ export const PendingStateCatcher = >(base: C) * async reference to the element that will be used to listen for pending state events * defaults to window */ - public accessor pendingStateCatcherTarget: Promise | null; + public accessor pendingStateCatcherTarget!: Promise | null; /** * @internal */ - #loadingDelayTimer: number; - #closeDelayTimer: number; - #openCount = 0; - #timeStampOfLoadingStart: number; + private loadingDelayTimer!: number; + private closeDelayTimer!: number; + private openCount = 0; + private timeStampOfLoadingStart!: number; + + private handlePendingStateEvent = async (e: Event) => { + const event = e as PendingStateEvent; + this.loadingStarted(); + this.openCount++; + try { + await event.detail.promise; + } catch { + // Do nothing, this will be handled by others + } finally { + this.openCount--; + if (this.openCount === 0) { + this.loadingStopped(); + } + } + }; async connectedCallback() { super.connectedCallback(); const target = (await this.pendingStateCatcherTarget) || window; - target.addEventListener(PendingStateEvent.eventType, this.#handlePendingStateEvent.bind(this)); + target.addEventListener(PendingStateEvent.eventType, this.handlePendingStateEvent); } async disconnectedCallback() { super.disconnectedCallback(); const target = (await this.pendingStateCatcherTarget) || window; - target.removeEventListener(PendingStateEvent.eventType, this.#handlePendingStateEvent.bind(this)); + target.removeEventListener(PendingStateEvent.eventType, this.handlePendingStateEvent); } - async #handlePendingStateEvent(e: PendingStateEvent) { - this.#loadingStarted(); - this.#openCount++; - try { - await e.detail.promise; - } catch { - // Do nothing, this will be handled by others - } finally { - this.#openCount--; - if (this.#openCount === 0) { - this.#loadingStopped(); - } - } - } - - #loadingStarted() { - window.clearTimeout(this.#loadingDelayTimer); + private loadingStarted() { + window.clearTimeout(this.loadingDelayTimer); //If new event is received while close timer is running, prevent the close - window.clearTimeout(this.#closeDelayTimer); + window.clearTimeout(this.closeDelayTimer); - this.#loadingDelayTimer = window.setTimeout(() => { - this.#timeStampOfLoadingStart = performance.now(); + this.loadingDelayTimer = window.setTimeout(() => { + this.timeStampOfLoadingStart = performance.now(); this.stateIsPending = true; }, this.pendingStateCatcherLoadingStartDelay); } - #loadingStopped() { - window.clearTimeout(this.#loadingDelayTimer); - const totalTimeOpened = performance.now() - this.#timeStampOfLoadingStart; + private loadingStopped() { + window.clearTimeout(this.loadingDelayTimer); + const totalTimeOpened = performance.now() - this.timeStampOfLoadingStart; const loadingStopDelay = Math.max(this.pendingStateCatcherMinTimeOpen - totalTimeOpened, 0); - this.#closeDelayTimer = window.setTimeout(() => { + this.closeDelayTimer = window.setTimeout(() => { this.stateIsPending = false; }, loadingStopDelay); } - }; + } + + return PendingStateCatcherClass as C & Constructor; +} diff --git a/packages/web/titanium/input-validator/filled-input-validator.ts b/packages/web/titanium/input-validator/filled-input-validator.ts index fe94334bd..8e999302b 100644 --- a/packages/web/titanium/input-validator/filled-input-validator.ts +++ b/packages/web/titanium/input-validator/filled-input-validator.ts @@ -14,7 +14,7 @@ import { MdFilledField } from '@material/web/field/filled-field'; @customElement('titanium-filled-input-validator') export class TitaniumFilledInputValidator extends MdFilledField { @property({ type: Boolean }) populated: boolean = true; - @property({ type: Object }) accessor evaluator: () => boolean; + @property({ type: Object }) accessor evaluator!: () => boolean; firstUpdated() { this.addEventListener('focusin', () => (this.focused = true)); diff --git a/packages/web/titanium/input-validator/input-validator.ts b/packages/web/titanium/input-validator/input-validator.ts deleted file mode 100644 index ca805acf8..000000000 --- a/packages/web/titanium/input-validator/input-validator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { customElement, property } from 'lit/decorators.js'; -import { MdOutlinedField } from '@material/web/field/outlined-field'; - -/** - * Input validator to make components use validation consistent with material 3 outlined styling - * - * @element titanium-input-validator - * @slot default - The slotted element should fire the NotifyUserInputEvent when it is ready to be validated - * @example - * (this.collection?.length ?? 0) > 0} validationMessage="Collection must have one item"> - * - * - */ -@customElement('titanium-input-validator') -export class TitaniumInputValidator extends MdOutlinedField { - @property({ type: Boolean }) populated: boolean = true; - @property({ type: Object }) accessor evaluator: () => boolean; - - firstUpdated() { - this.addEventListener('focusin', () => (this.focused = true)); - this.addEventListener('focusout', () => (this.focused = false)); - } - - reset() { - this.error = false; - } - - reportValidity() { - const valid = this.checkValidity(); - this.error = !valid; - - return valid; - } - - checkValidity() { - return !!this.evaluator(); - } -} diff --git a/packages/web/titanium/input-validator/outlined-input-validator.ts b/packages/web/titanium/input-validator/outlined-input-validator.ts deleted file mode 100644 index 3e6356cb3..000000000 --- a/packages/web/titanium/input-validator/outlined-input-validator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { customElement, property } from 'lit/decorators.js'; -import { MdOutlinedField } from '@material/web/field/outlined-field'; - -/** - * Input validator to make components use validation consistent with material 3 outlined styling - * - * @element titanium-outlined-input-validator - * @slot default - The slotted element should fire the NotifyUserInputEvent when it is ready to be validated - * @example - * (this.collection?.length ?? 0) > 0} validationMessage="Collection must have one item"> - * - * - */ -@customElement('titanium-outlined-input-validator') -export class TitaniumOutlinedInputValidator extends MdOutlinedField { - @property({ type: Boolean }) populated: boolean = true; - @property({ type: Object }) accessor evaluator: () => boolean; - - firstUpdated() { - this.addEventListener('focusin', () => (this.focused = true)); - this.addEventListener('focusout', () => (this.focused = false)); - } - - reset() { - this.error = false; - } - - reportValidity() { - const valid = this.checkValidity(); - this.error = !valid; - - return valid; - } - - checkValidity() { - return !!this.evaluator(); - } -} diff --git a/packages/web/titanium/profile-picture-stack/profile-picture-stack.ts b/packages/web/titanium/profile-picture-stack/profile-picture-stack.ts index 017f2b57e..1b855d414 100644 --- a/packages/web/titanium/profile-picture-stack/profile-picture-stack.ts +++ b/packages/web/titanium/profile-picture-stack/profile-picture-stack.ts @@ -19,7 +19,7 @@ export class TitaniumProfilePictureStack extends LitElement { /** * Array of people to display in a stack. */ - @property({ type: Array }) accessor people: Array>; + @property({ type: Array }) accessor people: Array> = []; /** * Number to define the max number of people to display in a stack. @@ -31,7 +31,7 @@ export class TitaniumProfilePictureStack extends LitElement { * This will open a new tab to the directory profile of the person. * This will only work if the person has an Id. */ - @property({ type: Boolean, attribute: 'enable-directory-href' }) accessor enableDirectoryHref: boolean = false; + @property({ type: Boolean, reflect: true, attribute: 'enable-directory-href' }) accessor enableDirectoryHref: boolean = false; /** * Toggle to show the full name of the person if there is one result in the stack. @@ -59,7 +59,7 @@ export class TitaniumProfilePictureStack extends LitElement { */ @state() private accessor autoMax: number = 0; - #resizeObserver: ResizeObserver; + #resizeObserver!: ResizeObserver; updated(changedProperties: PropertyValues) { if (changedProperties.has('autoResize')) { diff --git a/packages/web/titanium/search-input/filled-search-input.ts b/packages/web/titanium/search-input/filled-search-input.ts index e0dbd9ef0..30d694402 100644 --- a/packages/web/titanium/search-input/filled-search-input.ts +++ b/packages/web/titanium/search-input/filled-search-input.ts @@ -16,7 +16,7 @@ export default class TitaniumFilledSearchInput extends LitElement { @property({ type: Boolean }) accessor spellcheck: boolean = false; @property({ type: Boolean }) accessor disabled: boolean = false; - @query('md-filled-text-field') private accessor textField: MdFilledTextField; + @query('md-filled-text-field') private accessor textField!: MdFilledTextField; static styles = css` :host { diff --git a/packages/web/titanium/search-input/search-input.ts b/packages/web/titanium/search-input/search-input.ts deleted file mode 100644 index d7cf7f2d6..000000000 --- a/packages/web/titanium/search-input/search-input.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { PropertyValues, css, html, nothing } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; - -import '@material/web/iconbutton/icon-button'; -import '@material/web/icon/icon'; - -import { ExtendableOutlinedTextField } from '../extendable-outlined-text-field/extendable-outlined-text-field'; - -/** - * A styled input with built-in search and clear icons. . - * - * @element titanium-search-input - * - * @cssprop {Length} --titanium-search-input-expanded-width - Width when input expands - * - */ -@customElement('titanium-search-input') -export class TitaniumSearchInput extends ExtendableOutlinedTextField { - /** - * Whether or not the input should hide the clear button - */ - @property({ type: Boolean, attribute: 'hide-clear-button' }) accessor hideClearButton: boolean = false; - - /** - * Whether the input should prevent collapse. - */ - @property({ type: Boolean, reflect: true, attribute: 'prevent-collapse' }) accessor preventCollapse: boolean = false; - - @property({ type: Boolean, reflect: true, attribute: 'has-value' }) protected accessor hasValue: boolean = true; - - @property({ reflect: true, type: String }) accessor autocomplete: string = 'off'; - - @property({ reflect: true, type: Boolean }) accessor spellcheck: boolean = false; - - async updated(changedProps: PropertyValues) { - if (changedProps.has('value')) { - this.hasValue = !!this.value; - } - } - - static styles = css` - :host { - display: block; - cursor: pointer; - overflow: hidden; - } - - md-outlined-text-field { - width: 48px; - --md-outlined-field-outline-width: 0; - --md-outlined-field-hover-outline-width: 0; - --md-outlined-field-disabled-outline-width: 0; - - -webkit-transition: width 250ms 0ms cubic-bezier(0.4, 0, 0.2, 1); /* Safari */ - transition: width 250ms 0ms cubic-bezier(0.4, 0, 0.2, 1); - --md-outlined-text-field-bottom-space: 11px; - --md-outlined-text-field-top-space: 11px; - } - - :host([has-value]) md-icon-button[search], - :host([prevent-collapse]) md-icon-button[search], - md-outlined-text-field:focus-within md-icon-button[search] { - pointer-events: none; - } - - :host([has-value]) md-outlined-text-field, - :host([prevent-collapse]) md-outlined-text-field, - md-outlined-text-field:focus-within { - --md-outlined-field-outline-width: initial; - --md-outlined-field-disabled-outline-width: initial; - --md-outlined-field-hover-outline-width: initial; - --md-outlined-text-field-container-shape: initial; - - width: var(--titanium-search-input-expanded-width, 258px); - } - `; - - protected override renderMainSlot() { - return html` - - this.focus()} @focus=${() => this.focus()} slot="leading-icon"> - search - - ${!this.hasValue - ? nothing - : html` { - if (this.disabled) { - return; - } - this.input.focus(); - this.value = ''; - this.dispatchEvent(new Event('input')); - }} - > - close`} - `; - } -} diff --git a/packages/web/titanium/service-worker-notifier/service-worker-notifier.ts b/packages/web/titanium/service-worker-notifier/service-worker-notifier.ts deleted file mode 100644 index 23cc04412..000000000 --- a/packages/web/titanium/service-worker-notifier/service-worker-notifier.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { html, LitElement } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; -import { ShowSnackbarEvent } from '../../titanium/snackbar/show-snackbar-event'; - -@customElement('titanium-service-worker-notifier') -export class TitanuimServiceWorkerNotifier extends LitElement { - @property({ type: String }) accessor notificationsStatus: string; - @property({ type: String }) accessor scriptUrl: string = 'service-worker.js'; - - #newWorker: ServiceWorker | null; - #refreshing = false; - - async connectedCallback() { - if ('serviceWorker' in navigator) { - const reg = await navigator.serviceWorker.getRegistration(); - if (reg) { - reg.addEventListener('updatefound', () => { - this.#newWorker = reg.installing; - this.#newWorker?.addEventListener('statechange', () => { - if (this.#newWorker?.state === 'installed' && navigator.serviceWorker.controller) { - this.#showUpdatedSnackbar(); - } - }); - }); - - if (reg.waiting && navigator.serviceWorker.controller) { - this.#newWorker = reg.waiting; - this.#showUpdatedSnackbar(); - } - } - - navigator.serviceWorker.addEventListener('controllerchange', () => { - if (this.#refreshing) { - return; - } - window.location.reload(); - this.#refreshing = true; - }); - } - } - - async #showUpdatedSnackbar() { - await this.dispatchEvent(new ShowSnackbarEvent('Site has been updated', { actionText: 'RELOAD' })); - this.#newWorker?.postMessage({ type: 'SKIP_WAITING' }); - } - - render() { - return html``; - } -} diff --git a/packages/web/titanium/show-hide/show-hide.ts b/packages/web/titanium/show-hide/show-hide.ts index 3f18b2e6a..ebb7a2a5d 100644 --- a/packages/web/titanium/show-hide/show-hide.ts +++ b/packages/web/titanium/show-hide/show-hide.ts @@ -1,8 +1,6 @@ -import { css, LitElement, PropertyValues } from 'lit'; -import { literal, html } from 'lit/static-html.js'; +import { css, html, LitElement, PropertyValues } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; -import '@material/web/button/outlined-button'; import '@material/web/button/filled-button'; /** @@ -31,13 +29,9 @@ export default class TitaniumShowHide extends LitElement { @property({ type: Boolean, reflect: true, attribute: 'collapsed' }) accessor collapsed: boolean = true; @property({ type: Boolean, reflect: true, attribute: 'has-hidden-items' }) protected accessor hasHiddenItems: boolean = false; @property({ type: Number }) accessor hiddenItemCount: number = 0; - /** - * Swaps out outlined button for a filled button when true - */ - @property({ type: Boolean, attribute: 'filled' }) accessor filled: boolean = false; - @query('items-container') protected accessor itemsContainer: HTMLElement; - @query('collapsed-box') protected accessor collapsedContainer: HTMLElement; + @query('items-container') protected accessor itemsContainer!: HTMLElement; + @query('collapsed-box') protected accessor collapsedContainer!: HTMLElement; updated(changedProps: PropertyValues) { if (changedProps.has('collapsed')) { @@ -101,7 +95,6 @@ export default class TitaniumShowHide extends LitElement { gap: var(--titanium-show-hide-gap, 8px); } - md-outlined-button, md-filled-button { max-width: 160px; width: 100%; @@ -116,7 +109,6 @@ export default class TitaniumShowHide extends LitElement { ]; render() { - /* eslint-disable lit/binding-positions, lit/no-invalid-html */ return html`