diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4615bd94..0a0b99ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,10 +28,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 - uses: pnpm/action-setup@v3 with: - version: 9 + version: 9 - name: Setup Node uses: actions/setup-node@v4 with: @@ -41,8 +41,8 @@ jobs: uses: actions/configure-pages@v4 - name: Install dependencies run: pnpm install - - name: Analyze - run: pnpm analyze + - name: Building Packages + run: pnpm packages:build - name: Build Docs run: pnpm docs:build - name: Upload artifact @@ -60,4 +60,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 7bcf453b..e23b8348 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -13,6 +13,37 @@ flex-wrap: wrap; } +#usage { + background: var(--vp-c-bg-soft); + padding: 18px; + border-radius: 8px; + overflow: auto; + font-size: 14px; +} + +#usage li { + display: flex; + align-items: center; +} + +:not(.dark) #usage pre span { + color: var(--shiki-light, inherit); +} + +.dark #usage pre span { + color: var(--shiki-dark, inherit); +} + +#usage pre { + margin: 0; + flex-shrink: 0; +} + +#usage strong { + width: 90px; + flex-shrink: 0; +} + .table-wrapper { width: 100%; overflow: auto; @@ -22,3 +53,51 @@ min-width: 688px; display: table; } + +.modal-overlay { + position: fixed; + background: var(--vp-backdrop-bg-color); + backdrop-filter: blur(4px); + + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + + opacity: 0; + pointer-events: none; + transition: opacity 240ms; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-overlay.open { + opacity: 1; + pointer-events: auto; +} + +.modal { + overflow: auto; + background: var(--vp-c-bg); + padding: 0 30px 30px; + max-width: 90dvw; + width: 500px; + position: fixed; + top: 50%; + left: 50%; + z-index: 101; + transform: translate(-50%, -50%); + opacity: 0; + pointer-events: none; + transition: opacity 240ms; + border-radius: 16px; +} + +.modal.open { + transform: translate(-50%, -50%); + pointer-events: auto; + opacity: 1; +} + diff --git a/docs/components/[component].md b/docs/components/[component].md index 9d01ef08..13fd489a 100644 --- a/docs/components/[component].md +++ b/docs/components/[component].md @@ -4,33 +4,4 @@ next: false outline: "deep" --- - - diff --git a/docs/components/[component].paths.ts b/docs/components/[component].paths.ts index 701fbc1b..cc4f83b8 100644 --- a/docs/components/[component].paths.ts +++ b/docs/components/[component].paths.ts @@ -5,7 +5,13 @@ import { type Metadata, } from "../../internals/doc-helpers/types.ts"; import { getFileMeta } from "../../scripts/utils.ts"; -import { codify, tabulateData } from "../utils/md.ts"; +import { + codify, + getFormattedImportUsageString, + getFormattedTagUsageString, + getUsageSectionMarkdown, + tabulateData, +} from "../utils/markdown.ts"; export default { paths() { @@ -43,7 +49,13 @@ const getComponentMarkdown = (component: Component) => { res += `${component.summary}\n`; - res += getUsageMarkdown(component); + res += getUsageSectionMarkdown([ + [ + "Import", + getFormattedImportUsageString(component.importPaths.webComponents), + ], + ["Tag", getFormattedTagUsageString(component.tagName)], + ]); res += getMembersMarkdown(component); @@ -95,19 +107,6 @@ ${slots return res; }; -const getUsageMarkdown = (component: Component) => { - let res = ""; - - res += "\n\n"; - - return res; -}; - const getMembersMarkdown = (component: Component) => { const members = component.members || []; let res = ""; diff --git a/docs/icons.md b/docs/icons.md new file mode 100644 index 00000000..397e0436 --- /dev/null +++ b/docs/icons.md @@ -0,0 +1,113 @@ +--- +prev: false +next: false +outline: false +--- + + + + + +# Icons + +## Properties + +
+ + +| Name | Description | Default Value | +|------------|--------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| `viewbox` | The viewBox of the SVG. Allows you to redefine what the coordinates without units mean inside an SVG element. | `0 0 24 24` | +| `title` | Provides a human-readable title for the element that contains it. [More Info](https://www.w3.org/TR/SVG-access/#Equivalent) | - | +| `size` | The size of the icon. If set to `"auto"`, the icon will get the parent's width and height. | `"auto"` | + +
+ +## Explore Icons + + diff --git a/docs/internals/components/DocIconGrid.ts b/docs/internals/components/DocIconGrid.ts new file mode 100644 index 00000000..6926d1ad --- /dev/null +++ b/docs/internals/components/DocIconGrid.ts @@ -0,0 +1,286 @@ +import icons from "@tapsioss/icons"; +import * as scrollLock from "body-scroll-lock"; +import { createFocusTrap, type FocusTrap } from "focus-trap"; +import { html, LitElement, nothing, svg, type PropertyValues } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; + +import "vitepress/theme"; +import { + getFormattedImportUsageString, + getFormattedTagUsageString, + getUsageSectionMarkdown, +} from "../../utils/markdown.ts"; + +type SVGPathInfo = { + clipRule?: string; + fillRule?: string; + xlinkHref?: string; + d: string; +}; + +type SVGIconInfo = { + kebabName: string; + pascalName: string; + paths: SVGPathInfo[]; +}; + +@customElement("doc-icon-grid") +export class DocIconGrid extends LitElement { + protected override createRenderRoot() { + return this; + } + + @state() + _searchString: string = ""; + + @state() + _showModal: boolean = false; + + @state() + _selectedIcon: null | SVGIconInfo = null; + + @query("#icon-modal") + private _iconModal!: HTMLElement; + + private _focusTrapper: null | FocusTrap = null; + + private _handleInputChange(e: Event) { + this._searchString = (e.target as HTMLInputElement).value; + } + + private _normalizeStringFiltering(inputString: string): string { + return inputString.toLowerCase().replace(/-/g, "").replace(/_/g, ""); + } + + public override disconnectedCallback() { + super.disconnectedCallback(); + + scrollLock.enableBodyScroll(this._iconModal); + + this._focusTrapper?.deactivate(); + this._focusTrapper = null; + + window.removeEventListener("keydown", this._handleEscapeKey); + } + + protected override firstUpdated(_changedProperties: PropertyValues) { + super.firstUpdated(_changedProperties); + + this._focusTrapper = createFocusTrap(this._iconModal, { + clickOutsideDeactivates: true, + }); + + window.addEventListener("keydown", this._handleEscapeKey); + } + + private _handleEscapeKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + this._closeModal(); + } + }; + + private _closeModal = () => { + this._showModal = false; + this._selectedIcon = null; + scrollLock.enableBodyScroll(this._iconModal); + this._focusTrapper?.deactivate(); + }; + + private _openModal = (icon: SVGIconInfo) => { + this._showModal = true; + this._selectedIcon = icon; + scrollLock.disableBodyScroll(this._iconModal); + this._focusTrapper?.activate(); + }; + + private _getSvg(iconPaths?: SVGPathInfo[]) { + if (!iconPaths) return; + return html` + + `; + } + + private _renderIconUsageSection() { + if (!this._selectedIcon) return null; + + return unsafeHTML( + getUsageSectionMarkdown([ + [ + "Import", + getFormattedImportUsageString( + `@tapsioss/web-icons/${this._selectedIcon.kebabName}`, + ), + ], + [ + "Tag", + getFormattedTagUsageString( + `tapsi-icon-${this._selectedIcon.kebabName}`, + ), + ], + ]), + ); + } + + private _renderModal() { + return html` + + `; + } + + private _renderSearchIcon() { + return html``; + } + + private _renderSearchbar() { + return html`
+

Select an icon to view its details.

+ + + + +
`; + } + + private _shouldShowIconInGrid(icon: SVGIconInfo) { + // Do not apply the filter if the user typed a single-character string in search input. + if (this._searchString.length < 2) return true; + + return this._normalizeStringFiltering(icon.kebabName).includes( + this._normalizeStringFiltering(this._searchString), + ); + } + + private _renderIcons() { + const filteredIcons = Object.values( + icons as Record, + ).filter(icon => this._shouldShowIconInGrid(icon)); + + if (filteredIcons.length > 0) { + return filteredIcons.map(icon => { + return html``; + }); + } + + return html`
No icon was found for "${this._searchString}"!
`; + } + + protected override render() { + return html` + ${this._renderModal()}${this._renderSearchbar()} +
${this._renderIcons()}
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "doc-icon-grid": DocIconGrid; + } +} diff --git a/docs/package.json b/docs/package.json index a0775454..841c4629 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,6 +11,11 @@ "dependencies": { "@tapsioss/theme": "workspace:*", "@tapsioss/web-components": "workspace:*", - "@tapsioss/web-icons": "workspace:*" + "@tapsioss/icons": "workspace:*", + "body-scroll-lock": "4.0.0-beta.0", + "focus-trap": "^7.6.4" + }, + "devDependencies": { + "@types/body-scroll-lock": "^3.1.2" } } diff --git a/docs/sidebar.ts b/docs/sidebar.ts index 4af42288..e9b7deb5 100644 --- a/docs/sidebar.ts +++ b/docs/sidebar.ts @@ -10,18 +10,10 @@ const workspaceDir = path.join(dirname, ".."); const distDir = path.join(workspaceDir, "dist"); const metadataFile = path.join(distDir, "components-metadata.json"); -const getComponentsSidebarItems = (): DefaultTheme.SidebarItem[] => - (JSON.parse(fs.readFileSync(metadataFile).toString("utf-8")) as Metadata) - .sidebarItems; - -const getComponentsSidebar = (): DefaultTheme.Sidebar => { - return [ - { - text: "Components", - collapsed: false, - items: getComponentsSidebarItems(), - }, - ]; +const getSidebarItems = (): DefaultTheme.Sidebar => { + return ( + JSON.parse(fs.readFileSync(metadataFile).toString("utf-8")) as Metadata + ).sidebarItems; }; -export default getComponentsSidebar(); +export default getSidebarItems(); diff --git a/docs/utils/markdown.ts b/docs/utils/markdown.ts new file mode 100644 index 00000000..d7ced87d --- /dev/null +++ b/docs/utils/markdown.ts @@ -0,0 +1,39 @@ +export const tabulateData = (headers: string[], data: string[][]): string => { + if (headers.length === 0) return ""; + if (data.length === 0 || data[0]!.length === 0) return ""; + + const headersMarkdown = `| ${headers.join(" | ")} |`; + const separator = `| ${headers.map(() => "---").join(" | ")} |`; + + const rowsMarkdown = data.map(row => `| ${row.join(" | ")} |`).join("\n"); + + return `\n
\n\n${headersMarkdown}\n${separator}\n${rowsMarkdown}\n\n
\n`; +}; + +export const codify = (inputString: string) => `\`${inputString}\``; + +export const getFormattedTagUsageString = (tagName?: string) => { + if (!tagName) return ""; + return `
<${tagName}></${tagName}>
`; +}; + +export const getFormattedImportUsageString = (path?: string) => { + if (!path) return ""; + return `
import "${path}";
`; +}; + +export const getUsageSectionMarkdown = (sections: [string, string?][]) => { + let res = ""; + + if (sections.length === 0) return res; + + res += "\n\n"; + + return res; +}; diff --git a/docs/utils/md.ts b/docs/utils/md.ts deleted file mode 100644 index a05c0d95..00000000 --- a/docs/utils/md.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const tabulateData = (headers: string[], data: string[][]): string => { - if (headers.length === 0) return ""; - if (data.length === 0 || data[0]!.length === 0) return ""; - - const headersMarkdown = `| ${headers.join(" | ")} |`; - const separator = `| ${headers.map(() => "---").join(" | ")} |`; - - const rowsMarkdown = data.map(row => `| ${row.join(" | ")} |`).join("\n"); - - return `\n
\n\n${headersMarkdown}\n${separator}\n${rowsMarkdown}\n\n
\n`; -}; - -export const codify = (inputString: string) => `\`${inputString}\``; diff --git a/internals/doc-helpers/types.ts b/internals/doc-helpers/types.ts index ccb6b80e..9be7ba7a 100644 --- a/internals/doc-helpers/types.ts +++ b/internals/doc-helpers/types.ts @@ -31,34 +31,6 @@ export type InteractiveDemo = { slots: DemoSlot[]; }; -export type ComponentSlot = { - name: string; - description: string; -}; - -export type ComponentMember = { - type: { - text: string; - }; - description?: string; - name: string; - default?: string; - kind: string; -}; - -export type ComponentEvent = { - type?: { - text: string; - }; - description?: string; - name: string; -}; - -export type componentSuperclass = { - name: string; - module: string; -}; - export type ImportPaths = { webComponents?: string; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b836c5d..e6eb972a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,15 +170,25 @@ importers: docs: dependencies: + '@tapsioss/icons': + specifier: workspace:* + version: link:../packages/icons '@tapsioss/theme': specifier: workspace:* version: link:../packages/theme '@tapsioss/web-components': specifier: workspace:* version: link:../packages/web-components - '@tapsioss/web-icons': - specifier: workspace:* - version: link:../packages/web-icons + body-scroll-lock: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0 + focus-trap: + specifier: ^7.6.4 + version: 7.6.4 + devDependencies: + '@types/body-scroll-lock': + specifier: ^3.1.2 + version: 3.1.2 packages/icons: {} @@ -1222,6 +1232,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/body-scroll-lock@3.1.2': + resolution: {integrity: sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ==} + '@types/chai-dom@1.11.3': resolution: {integrity: sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==} @@ -1756,6 +1769,9 @@ packages: birpc@0.2.19: resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + body-scroll-lock@4.0.0-beta.0: + resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2371,6 +2387,9 @@ packages: focus-trap@7.6.4: resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==} + focus-trap@7.6.4: + resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==} + for-each@0.3.4: resolution: {integrity: sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==} engines: {node: '>= 0.4'} @@ -4948,6 +4967,8 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.17.16 + '@types/body-scroll-lock@3.1.2': {} + '@types/chai-dom@1.11.3': dependencies: '@types/chai': 4.3.20 @@ -5692,6 +5713,8 @@ snapshots: birpc@0.2.19: {} + body-scroll-lock@4.0.0-beta.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -6471,6 +6494,10 @@ snapshots: flatted@3.3.2: {} + focus-trap@7.6.4: + dependencies: + tabbable: 6.2.0 + focus-trap@7.6.4: dependencies: tabbable: 6.2.0 diff --git a/scripts/generate-docs-metadata.ts b/scripts/generate-docs-metadata.ts index efc302c1..4dc2dc52 100644 --- a/scripts/generate-docs-metadata.ts +++ b/scripts/generate-docs-metadata.ts @@ -20,9 +20,9 @@ const webComponentsSrcDir = path.join( "packages/web-components/src", ); -const distDir = path.join(workspaceDir, "dist"); -const metadataFile = path.join(distDir, "components-metadata.json"); -const cemFile = path.join(distDir, "custom-elements.json"); +const workspaceDistDir = path.join(workspaceDir, "dist"); +const metadataFile = path.join(workspaceDistDir, "components-metadata.json"); +const cemFile = path.join(workspaceDistDir, "custom-elements.json"); const cem = JSON.parse(fs.readFileSync(cemFile, "utf8")) as Package; @@ -35,7 +35,7 @@ const getKebabCaseComponentName = (component: Declaration) => { }; void (() => { - console.log("🧩 generating components metadata..."); + console.log("🧩 generating docs metadata..."); const sidebarItemsMap: Record = {}; const components: Component[] = []; @@ -109,7 +109,7 @@ void (() => { sidebarItem.text = childPath ?? relativePath; if (declarations.length === 1) { - sidebarItem.link = `components/${kebabCaseName}`; + sidebarItem.link = `/components/${kebabCaseName}`; } else { if (!Array.isArray(sidebarItem.items)) { sidebarItem.items = []; @@ -120,7 +120,7 @@ void (() => { return { text: name, - link: `components/${name}`, + link: `/components/${name}`, }; }); } @@ -148,20 +148,32 @@ void (() => { }); }); + const componentSidebarItems: DefaultTheme.SidebarItem = { + text: "Components", + items: Object.values(sidebarItemsMap).sort((a, b) => + a.text!.localeCompare(b.text!), + ), + }; + + const iconsSidebarItem: DefaultTheme.SidebarItem = { + text: "Icons", + link: "/icons", + }; + + const sidebarItems = [iconsSidebarItem, componentSidebarItems]; + fs.writeFileSync( metadataFile, JSON.stringify( { components, - sidebarItems: Object.values(sidebarItemsMap).sort((a, b) => - a.text!.localeCompare(b.text!), - ), + sidebarItems, }, null, 2, ), ); - console.log("✅ components metadata generated."); + console.log("✅ docs metadata generated."); })(); /* eslint-enable no-console */