;
+
+ constructor() {
+ super();
+ this.feedbackController = new UseMachine(this, {
+ machine: feedbackMachine,
+ options: { inspect }
+ });
+ }
+
+ #getMatches(match: 'prompt' | 'thanks' | 'form' | 'closed') {
+ return this.feedbackController.snapshot.matches(match);
+ }
+
+ override render() {
+ return html`
+ ${this.#getMatches('closed') ? this._closedTpl : this._feedbackTpl}
+ `;
+ }
+
+ get _feedbackTpl() {
+ return html`
+
+ ${this._closeFeedbackTpl}
+ ${this.#getMatches('prompt') ? this._promptTpl : ''}
+ ${this.#getMatches('thanks') ? this._thanksTpl : ''}
+ ${this.#getMatches('form') ? this._formTpl : ''} ${this._slotTpl}
+
+ `;
+ }
+
+ get _slotTpl() {
+ return html`
`;
+ }
+
+ get _closeFeedbackTpl() {
+ return html`
+
+
+
+ `;
+ }
+
+ get _promptTpl() {
+ return html`
+
+
How was your experience?
+
+
+
+
+
+ `;
+ }
+
+ get _thanksTpl() {
+ return html`
+
+
Thanks for your feedback.
+
+ ${this.feedbackController.snapshot.context.feedback
+ ? html`
"${this.feedbackController.snapshot.context.feedback}"
`
+ : ''}
+
+ `;
+ }
+
+ get _formTpl() {
+ return html`
+
+ `;
+ }
+
+ get _closedTpl() {
+ return html`
+
+ Feedback form closed.
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'lit-ts': LitTs;
+ }
+}
diff --git a/templates/lit-ts/src/LitTsCounter.ts b/templates/lit-ts/src/LitTsCounter.ts
new file mode 100644
index 0000000000..060383f441
--- /dev/null
+++ b/templates/lit-ts/src/LitTsCounter.ts
@@ -0,0 +1,95 @@
+import { html, LitElement } from 'lit';
+import { state } from 'lit/decorators.js';
+import { type InspectionEvent } from 'xstate';
+import { counterMachine } from './counterMachine.js';
+import { UseMachine } from '../../../packages/xstate-lit/src/index.js';
+// import { UseMachine } from '@xstate/lit';
+import { styles } from './styles/lit-ts-counter-styles.css.js';
+
+export class LitTsCounter extends LitElement {
+ static override styles = [styles];
+
+ #inspectEventsHandler: (inspEvent: InspectionEvent) => void =
+ this.#inspectEvents.bind(this);
+
+ #callbackHandler: (snapshot: SnapshotFrom) => void =
+ this.#callbackCounterController.bind(this);
+
+ counterController: UseMachine = new UseMachine(this, {
+ machine: counterMachine,
+ options: {
+ inspect: this.#inspectEventsHandler
+ },
+ callback: this.#callbackHandler
+ });
+
+ @state()
+ _xstate: typeof this.counterController.snapshot =
+ {} as unknown as typeof this.counterController.snapshot;
+
+ override updated(props: Map) {
+ super.updated && super.updated(props);
+ if (props.has('_xstate')) {
+ const { context, value } = this._xstate;
+ const detail = { ...(context || {}), value };
+ const counterEvent = new CustomEvent('counterchange', {
+ bubbles: true,
+ detail
+ });
+ this.dispatchEvent(counterEvent);
+ }
+ }
+
+ #callbackCounterController(snapshot: typeof this.counterController.snapshot) {
+ this._xstate = snapshot;
+ }
+
+ #inspectEvents(inspEvent: InspectionEvent) {
+ if (
+ inspEvent.type === '@xstate.snapshot' &&
+ inspEvent.event.type === 'xstate.stop'
+ ) {
+ this._xstate = {} as unknown as typeof this.counterController.snapshot;
+ }
+ }
+
+ get #disabled() {
+ return this.counterController.snapshot.matches('disabled');
+ }
+
+ override render() {
+ return html`
+
+
+
+
+
+
${this.counterController.snapshot.context.counter}
+
+
+
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'lit-ts-counter': LitTsCounter;
+ }
+}
diff --git a/templates/lit-ts/src/counterMachine.ts b/templates/lit-ts/src/counterMachine.ts
new file mode 100644
index 0000000000..bb4d0cf5cc
--- /dev/null
+++ b/templates/lit-ts/src/counterMachine.ts
@@ -0,0 +1,74 @@
+import { setup, assign } from 'xstate';
+
+/*
+ * This state machine represents a simple counter that can be incremented, decremented, and toggled on and off.
+ * The counter starts in the "enabled" state, where it can be incremented or decremented.
+ * If the counter reaches its maximum value, it cannot be incremented further. Similarly, if the counter reaches its minimum value, it cannot be decremented further. The counter can also be toggled to the "disabled" state, where it cannot be incremented or decremented.
+ * Toggling it again will bring it back to the "enabled" state.
+ */
+
+export const counterMachine = setup({
+ types: {
+ context: {} as { counter: number; event: unknown },
+ events: {} as
+ | {
+ type: 'INC';
+ }
+ | {
+ type: 'DEC';
+ }
+ | {
+ type: 'TOGGLE';
+ }
+ },
+ actions: {
+ increment: assign({
+ counter: ({ context }) => context.counter + 1,
+ event: ({ event }) => event
+ }),
+ decrement: assign({
+ counter: ({ context }) => context.counter - 1,
+ event: ({ event }) => event
+ })
+ },
+ guards: {
+ isNotMax: ({ context }) => context.counter < 10,
+ isNotMin: ({ context }) => context.counter > 0
+ }
+}).createMachine({
+ id: 'counter',
+ context: { counter: 0, event: undefined },
+ initial: 'enabled',
+ states: {
+ enabled: {
+ on: {
+ INC: {
+ actions: {
+ type: 'increment'
+ },
+ guard: {
+ type: 'isNotMax'
+ }
+ },
+ DEC: {
+ actions: {
+ type: 'decrement'
+ },
+ guard: {
+ type: 'isNotMin'
+ }
+ },
+ TOGGLE: {
+ target: 'disabled'
+ }
+ }
+ },
+ disabled: {
+ on: {
+ TOGGLE: {
+ target: 'enabled'
+ }
+ }
+ }
+ }
+});
diff --git a/templates/lit-ts/src/feedbackMachine.ts b/templates/lit-ts/src/feedbackMachine.ts
new file mode 100644
index 0000000000..a125e29308
--- /dev/null
+++ b/templates/lit-ts/src/feedbackMachine.ts
@@ -0,0 +1,64 @@
+import { assign, setup } from 'xstate';
+
+export const feedbackMachine = setup({
+ types: {
+ context: {} as { feedback: string },
+ events: {} as
+ | {
+ type: 'feedback.good';
+ }
+ | {
+ type: 'feedback.bad';
+ }
+ | {
+ type: 'feedback.update';
+ value: string;
+ }
+ | { type: 'submit' }
+ | {
+ type: 'close';
+ }
+ | { type: 'back' }
+ | { type: 'restart' }
+ },
+ guards: {
+ feedbackValid: ({ context }) => context.feedback.length > 0
+ }
+}).createMachine({
+ id: 'feedback',
+ initial: 'prompt',
+ context: {
+ feedback: ''
+ },
+ states: {
+ prompt: {
+ on: {
+ 'feedback.good': 'thanks',
+ 'feedback.bad': 'form'
+ }
+ },
+ form: {
+ on: {
+ 'feedback.update': {
+ actions: assign({
+ feedback: ({ event }) => event.value
+ })
+ },
+ back: { target: 'prompt' },
+ submit: {
+ guard: 'feedbackValid',
+ target: 'thanks'
+ }
+ }
+ },
+ thanks: {},
+ closed: {
+ on: {
+ restart: 'prompt'
+ }
+ }
+ },
+ on: {
+ close: '.closed'
+ }
+});
diff --git a/templates/lit-ts/src/index.ts b/templates/lit-ts/src/index.ts
new file mode 100644
index 0000000000..80cc5a4feb
--- /dev/null
+++ b/templates/lit-ts/src/index.ts
@@ -0,0 +1 @@
+export { LitTs } from './LitTs.js';
diff --git a/templates/lit-ts/src/styles/lit-ts-counter-styles.css.ts b/templates/lit-ts/src/styles/lit-ts-counter-styles.css.ts
new file mode 100644
index 0000000000..215dd0f6f7
--- /dev/null
+++ b/templates/lit-ts/src/styles/lit-ts-counter-styles.css.ts
@@ -0,0 +1,107 @@
+import { css } from 'lit';
+
+export const styles = css`
+ :host {
+ --color-primary: #056dff;
+ --_mark-color: rgb(197, 197, 197);
+ display: block;
+ box-sizing: border-box;
+ }
+
+ :host([hidden]),
+ [hidden] {
+ display: none !important;
+ }
+
+ *,
+ *::before,
+ *::after {
+ box-sizing: inherit;
+ }
+
+ ::slotted(*) {
+ display: block;
+ color: var(--color-primary);
+ white-space: nowrap;
+ text-indent: -1.5rem;
+ text-decoration: none;
+ margin-top: 0.5rem;
+ }
+
+ [aria-disabled='true'] {
+ opacity: 0.5;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ pointer-events: none;
+ cursor: not-allowed;
+ }
+
+ p {
+ font-size: 1.5rem;
+ min-width: 4.25rem;
+ text-align: center;
+ margin: auto;
+ padding: 0.8333em;
+ border-radius: 1rem;
+ border: 0.0625rem solid var(--_mark-color);
+ }
+
+ button {
+ appearance: none;
+ color: white;
+ border: none;
+ padding: 1rem 1.5rem;
+ border-radius: 0.25rem;
+ font: inherit;
+ cursor: pointer;
+ display: inline-block;
+ background-color: var(--color-primary);
+ }
+
+ button + button {
+ margin-top: 1rem;
+ }
+
+ div {
+ display: flex;
+ align-items: center;
+ max-width: 25rem;
+ padding: 1em 2em;
+ margin: auto;
+ background-color: rgb(238, 238, 238);
+ padding: 2rem;
+ background: white;
+ border-radius: 0.25rem;
+ box-shadow: 0 0.5rem 1rem #0001;
+ border: 0.0625rem solid var(--_mark-color);
+ border-bottom: none;
+ }
+ div + div {
+ position: relative;
+ border-top: 0.0625rem dashed var(--_mark-color);
+ border-bottom: 0.0625rem solid var(--_mark-color);
+ }
+
+ div + div button {
+ margin: 0 auto;
+ min-width: 10.625rem;
+ }
+
+ div + div span {
+ position: absolute;
+ display: block;
+ bottom: -1.5rem;
+ margin: 0;
+ }
+
+ span {
+ display: flex;
+ flex-direction: column;
+ margin-right: 2rem;
+ }
+
+ ::slotted(*) {
+ white-space: nowrap;
+ }
+`;
diff --git a/templates/lit-ts/src/styles/lit-ts-styles.css.ts b/templates/lit-ts/src/styles/lit-ts-styles.css.ts
new file mode 100644
index 0000000000..658e3919ac
--- /dev/null
+++ b/templates/lit-ts/src/styles/lit-ts-styles.css.ts
@@ -0,0 +1,93 @@
+import { css } from 'lit';
+
+export const styles = css`
+ :host {
+ --color-primary: #056dff;
+ display: block;
+ box-sizing: border-box;
+ display: block;
+ margin: auto;
+ }
+
+ :host([hidden]),
+ [hidden] {
+ display: none !important;
+ }
+
+ *,
+ *::before,
+ *::after {
+ box-sizing: inherit;
+ }
+
+ ::slotted(*) {
+ display: block;
+ color: var(--color-primary);
+ text-indent: 1rem;
+ white-space: nowrap;
+ text-decoration: none;
+ margin-top: 0.5rem;
+ }
+
+ em {
+ display: block;
+ margin-bottom: 1rem;
+ text-align: center;
+ }
+
+ .step {
+ padding: 2rem;
+ background: white;
+ border-radius: 1rem;
+ box-shadow: 0 0.5rem 1rem #0001;
+ width: 75vw;
+ max-width: 40rem;
+ }
+
+ .feedback {
+ position: relative;
+ }
+
+ .close-feedback {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ .close-button {
+ appearance: none;
+ background: transparent;
+ font: inherit;
+ cursor: pointer;
+ border: none;
+ padding: 1rem;
+ }
+
+ .button {
+ appearance: none;
+ color: white;
+ border: none;
+ padding: 1rem 1.5rem;
+ border-radius: 0.25rem;
+ font: inherit;
+ font-weight: bold;
+ cursor: pointer;
+ display: inline-block;
+ margin-right: 1rem;
+ background-color: var(--color-primary);
+ }
+
+ .button:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+
+ textarea {
+ display: block;
+ border: 2px solid #eaeaea;
+ border-radius: 0.25rem;
+ margin-bottom: 1rem;
+ width: 100%;
+ padding: 0.5rem;
+ }
+`;
diff --git a/templates/lit-ts/tsconfig.json b/templates/lit-ts/tsconfig.json
new file mode 100644
index 0000000000..ba77b6b018
--- /dev/null
+++ b/templates/lit-ts/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es2021",
+ "lib": ["es2021", "DOM", "DOM.Iterable"],
+ "experimentalDecorators": true,
+ "module": "es2020",
+ "moduleResolution": "node",
+ "rootDirs": ["./src", "./define"],
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true
+ },
+ "include": ["src/**/*.ts", "define/**/*.ts"]
+}
diff --git a/templates/lit-ts/vite.config.js b/templates/lit-ts/vite.config.js
new file mode 100644
index 0000000000..93b745228a
--- /dev/null
+++ b/templates/lit-ts/vite.config.js
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vite';
+import { rollupPluginHTML } from '@web/rollup-plugin-html';
+
+export default defineConfig({
+ plugins: [rollupPluginHTML()],
+ build: {
+ rollupOptions: {
+ input: 'demo/*.html',
+ output: {
+ format: 'es'
+ }
+ }
+ }
+});