diff --git a/packages/eslint-config/lit.json b/packages/eslint-config/lit.json new file mode 100644 index 0000000..9a214ca --- /dev/null +++ b/packages/eslint-config/lit.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "./base.json", + "plugin:wc/recommended", + "plugin:lit/recommended", + "./overwrites.json" + ] +} diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index b33484b..b691719 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -30,8 +30,10 @@ "eslint-config-prettier": "9.0.0", "eslint-config-turbo": "1.10.14", "eslint-plugin-jsx-a11y": "6.7.1", + "eslint-plugin-lit": "1.9.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", - "eslint-plugin-storybook": "0.6.14" + "eslint-plugin-storybook": "0.6.14", + "eslint-plugin-wc": "2.0.4" } } diff --git a/packages/web-components/.eslintignore b/packages/web-components/.eslintignore new file mode 100644 index 0000000..a890d4d --- /dev/null +++ b/packages/web-components/.eslintignore @@ -0,0 +1,4 @@ +node_modules +.turbo +dist +reports diff --git a/packages/web-components/.eslintrc.json b/packages/web-components/.eslintrc.json new file mode 100644 index 0000000..cb9538e --- /dev/null +++ b/packages/web-components/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "root": true, + "extends": ["@themeless-ui/eslint-config/lit.json"], + "env": { + "browser": true, + "es2022": true + } +} diff --git a/packages/web-components/LICENSE b/packages/web-components/LICENSE new file mode 100644 index 0000000..bd95074 --- /dev/null +++ b/packages/web-components/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 Joonas Tiala + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/web-components/package.json b/packages/web-components/package.json new file mode 100644 index 0000000..56d47eb --- /dev/null +++ b/packages/web-components/package.json @@ -0,0 +1,60 @@ +{ + "name": "@themeless-ui/web-components", + "version": "0.0.0", + "description": "ThemelessUI as Web Components", + "keywords": [ + "web-components", + "component-library", + "components" + ], + "author": "Joonas Tiala ", + "license": "MIT", + "homepage": "https://github.com/jtiala/themeless-ui", + "repository": { + "type": "git", + "url": "https://github.com/jtiala/themeless-ui", + "directory": "packages/web-components" + }, + "type": "module", + "module": "./dist/index.js", + "main": "./dist/index.umd.cjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + } + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "packageManager": "pnpm@8.7.6", + "scripts": { + "build": "vite build", + "lint": "pnpm run lint:tsc && pnpm run lint:eslint", + "lint:report": "pnpm run lint:tsc:report && pnpm run lint:eslint:report", + "lint:fix": "pnpm run lint:eslint:fix", + "lint:tsc": "tsc", + "lint:tsc:report": "mkdir -p ./reports && tsc > ./reports/lint-report-tsc.txt", + "lint:eslint": "eslint \"**/*.{ts,tsx}\"", + "lint:eslint:report": "eslint -f json -o ./reports/lint-report-eslint.json \"**/*.{ts,tsx}\"", + "lint:eslint:fix": "eslint --fix \"**/*.{ts,tsx}\"", + "clean": "rm -rf dist reports node_modules .turbo", + "publish:npm": "pnpm publish --access public" + }, + "peerDependencies": { + "lit": "^2" + }, + "dependencies": { + "@themeless-ui/style": "workspace:*", + "@themeless-ui/utils": "workspace:*" + }, + "devDependencies": { + "@themeless-ui/eslint-config": "workspace:*", + "@themeless-ui/typescript-config": "workspace:*", + "eslint": "8.50.0", + "typescript": "5.2.2", + "vite": "4.4.9" + } +} diff --git a/packages/web-components/src/components/Blockquote/Blockquote.ts b/packages/web-components/src/components/Blockquote/Blockquote.ts new file mode 100644 index 0000000..41f668a --- /dev/null +++ b/packages/web-components/src/components/Blockquote/Blockquote.ts @@ -0,0 +1,68 @@ +import { cn } from "@themeless-ui/utils"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { TUIComponent } from "../../utils"; + +const blockquoteClassName = cn("blockquote"); +const quoteClassName = cn("blockquote-quote"); +const authorClassName = cn("blockquote-author"); +const sourceClassName = cn("blockquote-source"); + +@customElement("tui-blockquote") +export class Blockquote extends TUIComponent { + /** + * A URI of the source of the information quoted. + */ + @property() + cite?: string; + + /** + * Author of the quotation. + */ + @property() + author?: string; + + /** + * Source of the quotation. + */ + @property() + source?: string; + + override render() { + const authorElement = this.author + ? html`—${this.author}` + : undefined; + + const sourceElement = this.source + ? html` + ${this.source} + ` + : undefined; + + const divider = + this.author && this.source ? html`, ` : undefined; + + const footer = + authorElement || sourceElement + ? html`` + : undefined; + + return html` +
+
+ ${footer} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "tui-blockquote": Blockquote; + } +} diff --git a/packages/web-components/src/components/Blockquote/index.ts b/packages/web-components/src/components/Blockquote/index.ts new file mode 100644 index 0000000..48f41ec --- /dev/null +++ b/packages/web-components/src/components/Blockquote/index.ts @@ -0,0 +1 @@ +export { Blockquote } from "./Blockquote"; diff --git a/packages/web-components/src/components/Heading/Heading.ts b/packages/web-components/src/components/Heading/Heading.ts new file mode 100644 index 0000000..97148bf --- /dev/null +++ b/packages/web-components/src/components/Heading/Heading.ts @@ -0,0 +1,35 @@ +/* eslint-disable lit/no-invalid-html */ +/* eslint-disable lit/binding-positions */ +import { cn } from "@themeless-ui/utils"; +import { customElement, property } from "lit/decorators.js"; +import { html, unsafeStatic } from "lit/static-html.js"; +import { TUIComponent } from "../../utils"; + +type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +const className = cn("heading"); + +@customElement("tui-heading") +export class Heading extends TUIComponent { + /** + * The level of the heading. + */ + @property({ type: Number }) + level!: HeadingLevel; + + override render() { + const tag = unsafeStatic(`h${this.level}`); + + return html` + <${tag} class="${className}" id="${this.id}" data-testid="${this.testId}"> + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "tui-heading": Heading; + } +} diff --git a/packages/web-components/src/components/Heading/index.ts b/packages/web-components/src/components/Heading/index.ts new file mode 100644 index 0000000..0776e15 --- /dev/null +++ b/packages/web-components/src/components/Heading/index.ts @@ -0,0 +1 @@ +export { Heading } from "./Heading"; diff --git a/packages/web-components/src/components/Paragraph/Paragraph.ts b/packages/web-components/src/components/Paragraph/Paragraph.ts new file mode 100644 index 0000000..c476da8 --- /dev/null +++ b/packages/web-components/src/components/Paragraph/Paragraph.ts @@ -0,0 +1,23 @@ +import { cn } from "@themeless-ui/utils"; +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { TUIComponent } from "../../utils"; + +const className = cn("paragraph"); + +@customElement("tui-paragraph") +export class Paragraph extends TUIComponent { + override render() { + return html` +

+ +

+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "tui-paragraph": Paragraph; + } +} diff --git a/packages/web-components/src/components/Paragraph/index.ts b/packages/web-components/src/components/Paragraph/index.ts new file mode 100644 index 0000000..cfb1958 --- /dev/null +++ b/packages/web-components/src/components/Paragraph/index.ts @@ -0,0 +1 @@ +export { Paragraph } from "./Paragraph"; diff --git a/packages/web-components/src/components/Text/Text.ts b/packages/web-components/src/components/Text/Text.ts new file mode 100644 index 0000000..b4937f0 --- /dev/null +++ b/packages/web-components/src/components/Text/Text.ts @@ -0,0 +1,91 @@ +/* eslint-disable lit/no-invalid-html */ +/* eslint-disable lit/binding-positions */ +import { cn } from "@themeless-ui/utils"; +import { customElement, property } from "lit/decorators.js"; +import { html, unsafeStatic } from "lit/static-html.js"; +import { TUIComponent } from "../../utils"; + +type TextType = + | "abbr" + | "b" + | "cite" + | "code" + | "del" + | "em" + | "i" + | "ins" + | "kbd" + | "mark" + | "q" + | "s" + | "samp" + | "small" + | "span" + | "strong" + | "sub" + | "sup" + | "u" + | "var"; + +const className = cn("text"); + +@customElement("tui-text") +export class Text extends TUIComponent { + /** + * The type of the rendered element. + * + * @default span + */ + @property() + type?: TextType; + + /** + * Only available when `type` is `abbr`. + * + * It's recommended to provide a full expansion of the abbreviated term in plain text on the first use of the abbreviation. + * If it's not viable to provide the expansion as full text, `title` should be used instead. + */ + // TODO: this shouldn't have ! + @property() + title!: string; + + /** + * Only available when `type` is `q`, `del`, or `ins`. + * + * `q`: A URI of the source of the information quoted. + * `del` or `ins`: A URI for a resource that explains the change. + */ + @property() + cite?: string; + + /** + * Only available when `type` is `del` or `ins`. + * + * Indicates the time and date of the change. Must be a valid date string with an optional time. + */ + @property() + dateTime?: string; + + override render() { + const tag = unsafeStatic(this.type || "span"); + + return html` + <${tag} + title="${this.title}" + cite="${this.cite}" + dateTime="${this.dateTime}" + class="${className}" + id="${this.id}" + data-testid="${this.testId}" + > + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "tui-text": Text; + } +} diff --git a/packages/web-components/src/components/Text/index.ts b/packages/web-components/src/components/Text/index.ts new file mode 100644 index 0000000..7afe56f --- /dev/null +++ b/packages/web-components/src/components/Text/index.ts @@ -0,0 +1 @@ +export { Text } from "./Text"; diff --git a/packages/web-components/src/components/index.ts b/packages/web-components/src/components/index.ts new file mode 100644 index 0000000..cbbf033 --- /dev/null +++ b/packages/web-components/src/components/index.ts @@ -0,0 +1,4 @@ +export * from "./Blockquote"; +export * from "./Heading"; +export * from "./Paragraph"; +export * from "./Text"; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts new file mode 100644 index 0000000..40b494c --- /dev/null +++ b/packages/web-components/src/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/packages/web-components/src/utils/TUIComponent.ts b/packages/web-components/src/utils/TUIComponent.ts new file mode 100644 index 0000000..e1af203 --- /dev/null +++ b/packages/web-components/src/utils/TUIComponent.ts @@ -0,0 +1,12 @@ +import { LitElement } from "lit"; +import { property } from "lit/decorators.js"; + +export class TUIComponent extends LitElement { + /** + * Provide `data-testid` attribute for the component to be used with testing tools. + * While the `testId` is a useful way for targeting the component, it should be used as a last resort. + * Whenever possible, use other attributes or selectors that have semantic meaning, such as `id` or `name`. + */ + @property() + testId?: string; +} diff --git a/packages/web-components/src/utils/index.ts b/packages/web-components/src/utils/index.ts new file mode 100644 index 0000000..984ed96 --- /dev/null +++ b/packages/web-components/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./TUIComponent"; diff --git a/packages/web-components/tsconfig.json b/packages/web-components/tsconfig.json new file mode 100644 index 0000000..999f187 --- /dev/null +++ b/packages/web-components/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@themeless-ui/typescript-config/vite-lit.json", + "include": ["src"] +} diff --git a/packages/web-components/vite.config.ts b/packages/web-components/vite.config.ts new file mode 100644 index 0000000..38d4397 --- /dev/null +++ b/packages/web-components/vite.config.ts @@ -0,0 +1,13 @@ +/// + +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + lib: { + entry: ["src/index.ts"], + name: "web-components", + fileName: "index", + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50f1503..17ff46f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,6 +272,9 @@ importers: eslint-plugin-jsx-a11y: specifier: 6.7.1 version: 6.7.1(eslint@8.50.0) + eslint-plugin-lit: + specifier: 1.9.1 + version: 1.9.1(eslint@8.50.0) eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.50.0) @@ -281,6 +284,9 @@ importers: eslint-plugin-storybook: specifier: 0.6.14 version: 0.6.14(eslint@8.50.0)(typescript@5.2.2) + eslint-plugin-wc: + specifier: 2.0.4 + version: 2.0.4(eslint@8.50.0) packages/react: dependencies: @@ -431,6 +437,34 @@ importers: specifier: 0.34.5 version: 0.34.5(jsdom@22.1.0) + packages/web-components: + dependencies: + '@themeless-ui/style': + specifier: workspace:* + version: link:../style + '@themeless-ui/utils': + specifier: workspace:* + version: link:../utils + lit: + specifier: ^2 + version: 2.8.0 + devDependencies: + '@themeless-ui/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@themeless-ui/typescript-config': + specifier: workspace:* + version: link:../typescript-config + eslint: + specifier: 8.50.0 + version: 8.50.0 + typescript: + specifier: 5.2.2 + version: 5.2.2 + vite: + specifier: 4.4.9 + version: 4.4.9(@types/node@20.4.7) + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -2953,6 +2987,16 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: true + /@lit-labs/ssr-dom-shim@1.1.1: + resolution: {integrity: sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==} + dev: false + + /@lit/reactive-element@1.6.3: + resolution: {integrity: sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==} + dependencies: + '@lit-labs/ssr-dom-shim': 1.1.1 + dev: false + /@mdx-js/esbuild@2.3.0(esbuild@0.18.16): resolution: {integrity: sha512-r/vsqsM0E+U4Wr0DK+0EfmABE/eg+8ITW4DjvYdh3ve/tK2safaqHArNnaqbOk1DjYGrhxtoXoGaM3BY8fGBTA==} peerDependencies: @@ -5857,6 +5901,10 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/trusted-types@2.0.4: + resolution: {integrity: sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==} + dev: false + /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} @@ -8797,6 +8845,18 @@ packages: semver: 6.3.0 dev: true + /eslint-plugin-lit@1.9.1(eslint@8.50.0): + resolution: {integrity: sha512-XFFVufVxYJwqRB9sLkDXB7SvV1xi9hrC4HRFEdX1h9+iZ3dm4x9uS7EuT9uaXs6zR3DEgcojia1F7pmvWbc4Gg==} + engines: {node: '>= 12'} + peerDependencies: + eslint: '>= 5' + dependencies: + eslint: 8.50.0 + parse5: 6.0.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + requireindex: 1.2.0 + dev: true + /eslint-plugin-react-hooks@4.6.0(eslint@8.50.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -8856,6 +8916,16 @@ packages: eslint: 8.50.0 dev: true + /eslint-plugin-wc@2.0.4(eslint@8.50.0): + resolution: {integrity: sha512-ORu7MBv0hXIvq894EJad70m+AvHGbmrDdKT6lcgtCVVhEbuIAyxg0ilfqqqHOmsh8PfcUBeEae3y7CElKvm1KQ==} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.50.0 + is-valid-element-name: 1.0.0 + js-levenshtein-esm: 1.2.0 + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -10841,6 +10911,12 @@ packages: engines: {node: '>=10'} dev: true + /is-valid-element-name@1.0.0: + resolution: {integrity: sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==} + dependencies: + is-potential-custom-element-name: 1.0.1 + dev: true + /is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} dev: true @@ -11627,6 +11703,10 @@ packages: '@sideway/pinpoint': 2.0.0 dev: true + /js-levenshtein-esm@1.2.0: + resolution: {integrity: sha512-fzreKVq1eD7eGcQr7MtRpQH94f8gIfhdrc7yeih38xh684TNMK9v5aAu2wxfIRMk/GpAJRrzcirMAPIaSDaByQ==} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -11954,6 +12034,28 @@ packages: - supports-color dev: true + /lit-element@3.3.3: + resolution: {integrity: sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==} + dependencies: + '@lit-labs/ssr-dom-shim': 1.1.1 + '@lit/reactive-element': 1.6.3 + lit-html: 2.8.0 + dev: false + + /lit-html@2.8.0: + resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==} + dependencies: + '@types/trusted-types': 2.0.4 + dev: false + + /lit@2.8.0: + resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==} + dependencies: + '@lit/reactive-element': 1.6.3 + lit-element: 3.3.3 + lit-html: 2.8.0 + dev: false + /local-pkg@0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} @@ -13736,13 +13838,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + dependencies: + parse5: 6.0.1 + dev: true + /parse5@5.1.0: resolution: {integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==} dev: true /parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: false /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}