diff --git a/.vscode/settings.json b/.vscode/settings.json index 502533627..edbe8556f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -278,6 +278,7 @@ "startdt", "statefulsets", "stddev", + "Svgs", "SVID", "SVIDs", "Swatinem", diff --git a/_TODO.md b/_TODO.md index d65e61196..18ce27977 100644 --- a/_TODO.md +++ b/_TODO.md @@ -33,33 +33,6 @@ We have E2E errors again testimonials slide on mobile chrome and safari. I think `test/e2e/specs/04-components/testimonials.spec.ts`:244:3 › Testimonials Component › @ready testimonials auto-rotate changes slide index -## Refactor of Common Code in Carousel related components - -See if there's any code in common between Carousel, Testimonials, and Themepicker. We have another one to add that uses the carousel code - for Skills. - -Issue identified with refactoring Testimonial and Carousel to Lit custom web components: - -The only thing to watch: the `transition:persist` directive must remain on the HTML custom element tag itself, not on an Astro component wrapper. That's already a project rule and isn't affected by `LitElement` vs `HTMLElement`. - -#### Selector files - -Carousel/client/selectors.ts and Testimonials/client/selectors.ts follow an identical pattern — only the CSS selectors and data-attribute names differ. The query/get function pairs are structurally identical. - -### What's genuinely different - -| Concern | Carousel | Testimonials | ThemePicker | -| -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| Embla options | [align: 'start'](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) | [align: 'center'](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) | [align: 'center'](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html), [containScroll](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html), no loop | -| Autoplay delay | 4000ms | 6000ms | None | -| Autoplay toggle button | ❌ | ✅ (play/pause icon swap) | ❌ | -| Keyboard nav (ArrowLeft/Right) | ✅ | ❌ | ❌ | -| Status region (`"Slide X of Y"`) | ✅ | ❌ | ❌ | -| Dot styling | [is-active](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) class | width-based (`w-3` → `w-6`) | None | -| Viewport ID / `aria-controls` | ❌ | ✅ | ❌ | -| View Transitions integration | ❌ | ❌ | ✅ (Lit + `astro:after-swap`) | -| Tooltip portal | ❌ | ❌ | ✅ | -| Base class | [HTMLElement](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) | [HTMLElement](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) | [LitElement](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) | - ## Youtube video for Backstage IDP hero on Home page Discuss agentic AI integrations to add to Backstage @@ -150,7 +123,6 @@ Need a tooltip component for consistency. List to add tooltips to: - /downloads/[slug] - /newsletter/confirm/[token] - /offline -- /privacy - /privacy/my-data - /search - /tags/[tag] @@ -165,77 +137,67 @@ Need a tooltip component for consistency. List to add tooltips to: ## Non-featured tags -Generate cover images for the skills and technologies tags pages: - -argo-cd -aws -azure -backstage -crossplane -docker -dotnet -go -grafana -helm -kubernetes -openstack -prometheus / promql -python -ruby -terraform -typescript - -## Stylings - -- Add 'featured' to tags to filter, and move tags page to articles +Generate cover images for the skills and technologies tags pages -- Need to make tables responsive on mobile +## Code Block Problems -- Need to remove automatic abbr handling when the abbreviation is used in a header, also the abbr presentation needs improved - right now it gives a question mark pointer and long delay to appear +- Inline code blocks are not wrapping. They're breaking to a new line. An example is in the "Scenario: CRD Sync Order Problem" section of `argocd-sync-failures-gitops-debugging-troubleshooting#specific-failure-scenarios`. -- When adding backticks in a callout, it gets the standard grey background for a code block in light theme. But it should get an offset of the callout color, like "info-offset". +- Need to allow escaping a code fence inside a markdown code fence, see `src/content/articles/api-gateway-metrics-traces-logs-debugging/index.mdx` "Latency Spike Investigation" section and the demo article. + +- I aliased 'promql' to 'go'. When a code fence using the alias is rendered with the language set to 'promql', it shows as 'go' incorrectly because of the alias. Also we need custom handling for all language names that are displayed: html should be uppercase, typescript as TypeScript, etc. Also we don't want all aliased names to show the alias - for example using the aliases 'ts', 'js', and 'md' would be better to show the full language names. + +- The "go" language code block in `src/content/articles/api-versioning-deprecation-sunset-headers-migration/index.mdx` is not being combined with the "typescript", "python", and "ruby" tabbed code block. Same with "python" block in "### Admission Control Strategies" section of `src/content/articles/backpressure-load-shedding-admission-control-overload/index.mdx`, plus the line numbering is weird in the code block. + +- The title for a code block with yaml as the language type in "Sync wave annotations controlling resource application order" section of `src/content/articles/argocd-sync-failures-gitops-debugging-troubleshooting/index.mdx` is not displaying, it's showing the YAML language tag instead of the title. Also, the word "app" is being highlighted for some reason in this code block. -- Inline code blocks are not wrapping. They're breaking to a new line. An example is in the "Scenario: CRD Sync Order Problem" section of argocd-sync-failures-gitops-debugging-troubleshooting#specific-failure-scenarios. +- Code block formatting for 'text' language is very plain. + +- The "❌" symbol is not showing in sql code block, it's rendering as a plain "X". But "✅" renders okay. `src/content/articles/database-schema-migrations-continuous-deployment-zero-downtime/index.mdx` -- The spacing on unordered task lists nested in an ordered list is wrong, see "Questions to Ask Before Setting Targets" in src/content/articles/availability-targets-five-nines-cost-benefit-analysis/index.mdx +- Language "haproxy" is not highlighting Shiki, need to adjust allowed languages in config + +- add "cel" language to code blocks + +## Callout Issues + +- When adding backticks in a callout, it gets the standard grey background for a code block in light theme. But it should get an offset of the callout color, like "info-offset". - There's too much space at the bottom of callouts, a full line of extra space in the colored background area. -## Project Stuff +## OL / UL Lists -- Re-enable link validator in astro.config.ts when pdf / downloads sorted out -- Need to allow escaping a code fence inside a markdown code fence, see src/content/articles/api-gateway-metrics-traces-logs-debugging/index.mdx "Latency Spike Investigation" section and the demo article. -- I aliased 'promql' to 'go'. When a code fence using the alias is rendered with the language set to 'promql', it shows as 'go' incorrectly because of the alias. Also we need custom handling for all language names that are displayed: html should be uppercase, typescript as TypeScript, etc. Also we don't want all aliased names to show the alias - for example using the aliases 'ts', 'js', and 'md' would be better to show the full language names. -- The "go" language code block in src/content/articles/api-versioning-deprecation-sunset-headers-migration/index.mdx is not being combined with the "typescript", "python", and "ruby" tabbed code block. Same with "python" block in "### Admission Control Strategies" section of src/content/articles/backpressure-load-shedding-admission-control-overload/index.mdx, plus the line numbering is weird in the code block. +- The spacing on unordered task lists nested in an ordered list is wrong, see "Questions to Ask Before Setting Targets" in `src/content/articles/availability-targets-five-nines-cost-benefit-analysis/index.mdx` + +- Numbering for ordered lists breaks when there are code blocks in between numbered list elements, like in `src/content/articles/cdn-edge-caching-cache-keys-vary-headers/index.mdx` + +## Checklist + +- Need to make tables responsive on mobile +- Need to stop system from adding abbreviation html when used in headings, also the `abbr` presentation needs improved - right now it gives a question mark pointer and long delay to appear +- Re-enable link validator in `astro.config.ts` when pdf / downloads sorted out - Add people who sign up for newsletter, download, or fill out contact form to Hubspot tracking. Need to configure it to remove them if they do the GDPR remove me. Also remove them from the newsletter. -- The title for a code block with yaml as the language type in "Sync wave annotations controlling resource application order" section of src/content/articles/argocd-sync-failures-gitops-debugging-troubleshooting/index.mdx is not displaying, it's showing the YAML language tag instead of the title. Also, the word "app" is being highlighted for some reason in this code block. -- Code block formatting for 'text' language is very plain. - Mathjax not working on inline formulas: "Where $L$ is the average number of items in the system (queue depth), $\lambda$ is the arrival rate (requests per second), and $W$ is the average time in system (latency)." In backpressure-load-shedding-admission-control-overload, also "The Retry Amplification Problem" section in circuit-breaker-retry-budget-cascade-failure-prevention. -- Uppy, Tus server, whatever other server needed -- Need a Q & A format to use in blameless-postmortem-incident-analysis-systemic-causes -- Numbering for ordered lists breaks when there are code blocks in between numbered list elements, like in src/content/articles/cdn-edge-caching-cache-keys-vary-headers/index.mdx +- Uppy, Tus server, whatever other server needed for file upload on Contact Form component +- Need a Q & A format to use in `blameless-postmortem-incident-analysis-systemic-causes` - Add a cloud of tags on articles list view at top for quick navigation. Add a ToC for featured tags so it's available on mobile, but with something different on desktop view - maybe hide tag cloud on mobile, and show it with an HR between all tags cloud and featured on desktop. -- The "❌" symbol is not showing in sql code block, it's rendering as a plain "X". But "✅" renders okay. src/content/articles/database-schema-migrations-continuous-deployment-zero-downtime/index.mdx - Use an in-project Image component to wrap Astro's Image and Picture. Show a magnifying glass with a "+" for the cursor on hover, and a modal to show a magnified view of images on click. - Add a copyright notice to content -- Probably a duplicate, but need to stop system from adding abbreviation html when used in headings -- DownloadLayout to wrap downloads.mdx in each folder, generate content for each -- Time in prose is causing a line break, and the colon and minutes to be removed - "2:47 AM" in src/content/articles/mtls-certificate-rotation-service-mesh-authentication/pdf.mdx -- Language "haproxy" is not highlighting Shiki, need to adjust allowed languages in config +- DownloadLayout to wrap `downloads.mdx` in each folder +- Image for home page newsletter CTA +- Time in prose is causing a line break, and the colon and minutes to be removed - "2:47 AM" in `src/content/articles/mtls-certificate-rotation-service-mesh-authentication/pdf.mdx`. Times like "11:59:59" are breaking across two lines - `src/content/articles/rate-limiting-token-bucket-leaky-bucket-implementation/index.mdx` in "Algorithm Overview" section - Should we exclude "Footnotes" from the ToC list? Right now it shows at the bottom if there's a Footnotes H2. - If a reader has already given their email address - newsletter signup, contact form, download registration, then the download CTA on short form articles should go directly to the HTML version of the deep dive, and it should have a PDF download button. Think this workflow through - maybe a "Short / Deep-Dive" slider button on top and don't show the CTA + the PDF download button. - The articles list page should show tags at top for quick navigation. There's another note about this. Show the count of articles per tag unless they're all the same on the tag. -- Maybe duplicate - scrunch the header and set at absolute position when you scroll down, expand back when scroll up - 404 page should show search results based on query -- Times like "11:59:59" are breaking across two lines - src/content/articles/rate-limiting-token-bucket-leaky-bucket-implementation/index.mdx in "Algorithm Overview" section -- Remove the dynamic Services page, and show it all on a single page. Three services: full time workflow, part time workflow, on-call support + onboarding fee. -- add "cel" language to code blocks -- hovering menu items should cause the dots to disappear immediately and then the animation slide to start - Update EXIF data on all AI generated JPGs +- When you click a link to a heading anchor, the heading is hidden by the page header now that it's stickied to the page ## Header -Need to improve the "squish" animation where the header reduces in size on scroll down, and returns to full size on scroll up. Maybe reduce and expand the text and search / themepicker / hamburger menu sizes in place, and then slide them horizontally. The relevant CSS classes: +- Need to improve the "squish" animation where the header reduces in size on scroll down, and returns to full size on scroll up. Maybe reduce and expand the text and search / themepicker / hamburger menu sizes in place, and then slide them horizontally. + +- The right-hand browser vertical scroll bar is hidden under the header ## Reading position indicator @@ -244,11 +206,17 @@ Need to improve the "squish" animation where the header reduces in size on scrol ## Print - Need a workflow to generate PDF files from Markdown for downloads. + - Add a QR code at the bottom of printed pages so it's easier for someone to navigate to from a printed page. + - Need a layout alternative to Markup that formats for print. It needs to handle TOC differently as a full-width page. Need a fixed cover page format that adds article title, subtitle, and date. + - We have two print scenarios: black and white, and color for PDF output. Can use two different media queries to accomplish getting colored variables. + - Generate mermaid graphs for PDF from AI image generator to improve looks. + - Need to make sure that on print, when we have a tabbed code block with multiple languages, only the first language is printed and the other language tabs are hidden. The styling should be different for print for the code block. Maybe move other language code tabs to an appendix and add a link to them. + - Need to only load print style sheet when needed. [Paged.js](https://pagedjs.org/en/documentation/) polyfills `@page` properties, and lays out an HTML document in print format where it can have page numbers generated to update in a table of contents. @@ -261,16 +229,15 @@ This article has different approaches to [print pagination](https://www.customjs ## Downloads / Gated Content - The PDF downloads are going to be gated - the user has to sign up for them and give an email address. How do we handle the keywords in these from a search perspective? + - We need a gating system, where the user gets a token to be able to download a PDF and the token is checked before downloading. ## Content Issues - "### Geographic/Currency Mismatches" in src/content/articles/cdn-edge-caching-cache-keys-vary-headers/index.mdx + - cover.jpg for reliability-and-testing needs touch up in GIMP -- We need to check for short form and deep article articles where the deep-dive index.pdf has a non-featured tag lik "argo-cd" only in the pdf.mdx. In those cases, we should make sure the callout for the deep dive includes the name of that non-featured (technology) tag and add the name to the tags: frontmatter key in the index.mdx -- Need an article on OpenStack -## focus-visible +- We need to check for short form and deep article articles where the deep-dive index.pdf has a non-featured tag lik "argo-cd" only in the pdf.mdx. In those cases, we should make sure the callout for the deep dive includes the name of that non-featured (technology) tag and add the name to the tags: frontmatter key in the index.mdx -- yellow outline on "Let's talk ->" in backstage component is rounded like the button -- yellow outlines on icon cards in skills + technologies are clipped at the top, and rounded on the play/pause button +- Need an article on OpenStack diff --git a/src/components/CallToAction/Download/index.astro b/src/components/CallToAction/Download/index.astro index ec03def76..d590676b2 100644 --- a/src/components/CallToAction/Download/index.astro +++ b/src/components/CallToAction/Download/index.astro @@ -4,6 +4,8 @@ * download page, which is implemented by Forms/Download. */ import { BuildError } from '@lib/errors/BuildError' +import List from '@components/List/index.astro' +import Marker from '@components/List/marker.astro' export interface Props { /** Optional base id for internal aria relationships */ @@ -14,6 +16,10 @@ export interface Props { title?: string /** Supporting description text */ description?: string + /** Optional List */ + listItems?: { + text: string + }[] /** Download button text */ buttonText?: string /** Optional secondary action button */ @@ -28,8 +34,12 @@ const { resource, title = 'Download Our Free Resource', description = 'Get instant access to expert insights and actionable strategies.', + listItems, buttonText = 'Download Now', - secondaryLink, + secondaryLink = { + href: "/services/consulting", + text: "Learn More" + }, } = Astro.props const normalizedResource = typeof resource === 'string' ? resource.trim() : '' @@ -45,147 +55,79 @@ const downloadUrl = `/downloads/${normalizedResource}` ---
- -
-
-
-
-
-
- -
-
- -
-
+
+ - - + + Free PDF Guide +
-

+

{title}

-

+

{description}

-
- - - {buttonText} - - { - secondaryLink && ( - - {secondaryLink.text} - - ) - } -
+

What you'll get:

+ { + listItems && ( + + ) + } +
- -
-
- - Instant Access +
+
+
+
+ +
+

Free resource · Instant access

-
- - No Credit Card Required -
-
- - Expert Insights + + {buttonText} + + { + secondaryLink && ( + + {secondaryLink.text} + + ) + }
+ +

No credit card required.

diff --git a/src/components/Carousel/index.astro b/src/components/Carousel/index.astro index be854f30c..d24569100 100644 --- a/src/components/Carousel/index.astro +++ b/src/components/Carousel/index.astro @@ -43,6 +43,8 @@ export type Props = items: ItemType[] }) +const props: Props = Astro.props + const { title, titleHref, @@ -53,11 +55,13 @@ const { items: itemsOverride, navigationMode = 'wrap', showReadMore = false, -} = Astro.props +} = props + +const isKnownCollectionSlug = (slug: Props['type']): slug is KnownCollectionSlug => slug !== 'unknown' const allItems: ItemType[] = itemsOverride ? itemsOverride - : type === 'unknown' + : !isKnownCollectionSlug(type) ? (() => { throw new Error('Carousel: type="unknown" requires `items` to be provided') })() diff --git a/src/components/Layout/HeadingAnchor/index.astro b/src/components/Layout/HeadingAnchor/index.astro new file mode 100644 index 000000000..6d5c5445c --- /dev/null +++ b/src/components/Layout/HeadingAnchor/index.astro @@ -0,0 +1,9 @@ +--- +export type Props = { + href: string +} + +const { href } = Astro.props +--- + + diff --git a/src/components/Layout/Privacy/index.astro b/src/components/Layout/Privacy/index.astro new file mode 100644 index 000000000..bbd5cefbd --- /dev/null +++ b/src/components/Layout/Privacy/index.astro @@ -0,0 +1,570 @@ +--- +import contactData from '@content/contact.json' +import List from '@components/List/index.astro' +import Marker from '@components/List/marker.astro' +import HeadingAnchor from '@components/Layout/HeadingAnchor/index.astro' + +const lastUpdatedDate = new Date() +const lastUpdatedIso = lastUpdatedDate.toISOString() +const lastUpdatedText = lastUpdatedDate.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', +}) + +export type Props = { + content: { + header: { + title: string + description: string + } + nav: { + title: string + items: { + label: string + href: string + }[] + } + collection: { + title: string + cards: { + title: string + description: string + listItems: { + text: string + icon: string + color: string + }[] + listMarkerSize?: number + trailer?: string + }[] + } + usage: { + title: string + description: string + listItems: { + title: string + description: string + icon: string + color: string + background: string + }[] + listMarkerSize?: number + } + sharing: { + title: string + subtitle: string + description: string + listItems: { + text: string + color: string + }[] + } + security: { + title: string + description: string + cards: { + lead: string + text: string + icon: string + color: string + background: string + }[] + } + rights: { + title: string + description: string + cards: { + lead: string + text: string + icon: string + color: string + }[] + } + cookies: { + title: string + description: string + subtitle: string + listItems: { + lead: string + text: string + color: string + }[] + } + storage: { + title: string + description: string + subtitle: string + listItems: { + lead: string + text: string | string[] + color: string + }[] + trailer: string + } + monitoring: { + title: string + description: string + list1: { + lead: string + listItems: { + heading: string + text: string + }[] + } + list2: { + lead: string + listItems: { + text: string + }[] + } + close: { + heading: string + text: string + } + } + contact: { + title: string + description: string + card1: { + lead: string + text: string + link: string + }, + card2: { + lead: string + text: string + } + card3: { + lead: string + text: string + } + } + changes: { + title: string + description: string + } + } +} + +type NavItem = Props['content']['nav']['items'][number] +type CollectionCard = Props['content']['collection']['cards'][number] + +const { content }: Props = Astro.props + +const navItems: Array = [] + +for (const [index, item] of content.nav.items.entries()) { + navItems.push({ + ...item, + showSeparator: index < content.nav.items.length - 1, + }) +} +--- + +
+
+
+ +
+

{content.header.title}

+

+ {content.header.description} +

+

+ Last updated: +

+
+ + + +
+
+

+ {content.collection.title} + +

+ +
+ { + content.collection.cards.map((card: CollectionCard) => ( +
+

+ {card.title} +

+

+ {card.description} +

+ + {card.trailer && ( +

+ {card.trailer} +

+ )} +
+ )) + } +
+
+ +
+

+ {content.usage.title} + +

+ +
+ { + content.usage.listItems.map((item) => ( +
+
+
+ +
+

{item.title}

+
+

+ {item.description} +

+
+ )) + } +
+
+ +
+

+ {content.sharing.title} + +

+
+
+
+ +
+
+

{content.sharing.subtitle}

+

{content.sharing.description}

+ +
+
+
+
+ +
+

+ {content.security.title} + +

+

+ {content.security.description} +

+
+ { + content.security.cards.map((item) => ( +
+
+ +
+

{item.lead}

+

+ {item.text} +

+
+ )) + } +
+
+ +
+

+ {content.rights.title} + +

+
+

+ {content.rights.description} +

+
+ { + content.rights.cards.map((item) => ( +
+
+ +
+
+

{item.lead}

+

{item.text}

+
+
+ )) + } +
+
+
+ +
+

+ {content.cookies.title} + +

+

+ {content.cookies.description} +

+
+

+ {content.cookies.subtitle} +

+ +
+
+ +
+

+ {content.storage.title} + +

+

+ {content.storage.description} +

+
+

+ {content.storage.subtitle} +

+
+ +
+
+

+
+
+
+ +
+

+ {content.monitoring.title} + +

+

+ {content.monitoring.description} +

+
+

+ {content.monitoring.list1.lead} +

+ ({ + lead: item.heading, + text: item.text, + color: "bg-blue-600 dark:bg-blue-400", + }))} + classes={{ + ul: "pl-9", + }} + /> +
+
+

+ + {content.monitoring.list2.lead} +

+ +
+
+

+ {content.monitoring.close.heading} +

+

+ {content.monitoring.close.text} +

+
+
+ +
+

+ {content.contact.title} + +

+
+

+ {content.contact.description} +

+
+
+
+ +
+
+

+ {content.contact.card1.lead} +

+ + {content.contact.card1.text} + +
+
+
+
+ +
+
+

+ {content.contact.card2.lead} +

+

+ {content.contact.card2.text} +

+
+
+
+ +
+

+ {content.contact.card3.lead} +

+

+ {content.contact.card3.text} +

+
+

+ {contactData.company.dataProtectionOfficer.name} +

+ + {contactData.company.dataProtectionOfficer.email} + +
+
+
+
+ +
+

+ {content.changes.title} + +

+
+

+ {content.changes.description} +

+
+
+
+
diff --git a/src/components/List/index.astro b/src/components/List/index.astro new file mode 100644 index 000000000..68be264d5 --- /dev/null +++ b/src/components/List/index.astro @@ -0,0 +1,61 @@ +--- +import AccentBorderLeftList from '@components/List/layouts/AccentBorderLeftList.astro' +import BadgeList from '@components/List/layouts/BadgeList.astro' +import CardGridList from '@components/List/layouts/CardGridList.astro' +import CheckIconsList from '@components/List/layouts/CheckIconsList.astro' +import ChevronList from '@components/List/layouts/ChevronList.astro' +import ColoredMarkerList from '@components/List/layouts/ColoredMarkerList.astro' +import NumberedWithBackgroundList from '@components/List/layouts/NumberedWithBackgroundList.astro' +import PlainIconList from '@components/List/layouts/PlainIconList.astro' +import SideBySideList from '@components/List/layouts/SideBySideList.astro' +import TimelineList from '@components/List/layouts/TimelineList.astro' +import TwoColumnCheckIconsList from '@components/List/layouts/TwoColumnCheckIconsList.astro' +import ZebraList from '@components/List/layouts/ZebraList.astro' + +export type Props = { + items: { + title?: string + lead?: string + text: string + icon?: string + color?: string + }[] + classes?: { + ol?: string + ul?: string + li?: string + em?: string + } + size?: number + variant: string +} + +const { items, size, variant = 'default', classes } = Astro.props +const classesProps = classes ? { classes } : {} +const sizeProps = size !== undefined ? { size } : {} +const itemsWithColor = items as Array<{ + title?: string + lead?: string + text: string + icon?: string + color: string +}> +const itemsWithIconAndColor = items as Array<{ + text: string + icon: string + color: string +}> +--- + +{variant === 'accent-border-left-list' && } +{variant === 'badge-list' && } +{variant === 'card-grid-list' && } +{variant === 'check-icons-list' && } +{variant === 'chevron-list' && } +{variant === 'colored-marker-list' && } +{variant === 'numbered-with-background-list' && } +{variant === 'plain-icon-list' && } +{variant === 'side-by-side-list' && } +{variant === 'timeline-list' && } +{variant === 'two-column-check-icons-list' && } +{variant === 'zebra-list' && } diff --git a/src/components/List/layouts/AccentBorderLeftList.astro b/src/components/List/layouts/AccentBorderLeftList.astro new file mode 100644 index 000000000..422b9f1bd --- /dev/null +++ b/src/components/List/layouts/AccentBorderLeftList.astro @@ -0,0 +1,40 @@ +--- +export type Props = { + items: { + lead?: string + text: string | string[] + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = "space-y-4 list-none pl-0" +const liClass = "border-l-4 pl-4" +const headerClass = "text-content-active font-semibold mb-2" +const textClass = "text-content-offset text-sm" +--- + +
    + { + items.map((item) => ( +
  • +

    {item.lead}

    + {Array.isArray(item.text) ? ( +
    + {item.text.map((paragraph) => ( +

    + ))} +
    + ) : ( +

    + )} +
  • + )) + } +
diff --git a/src/components/List/layouts/BadgeList.astro b/src/components/List/layouts/BadgeList.astro new file mode 100644 index 000000000..4ae796f98 --- /dev/null +++ b/src/components/List/layouts/BadgeList.astro @@ -0,0 +1,37 @@ +--- +export type Props = { + items: { + title?: string + lead?: string + text: string + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = ["space-y-4", classes?.ul] +const liClass = ["flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4", classes?.li] +const titleClass = "shrink-0 px-2 py-1 rounded bg-content text-page-base font-mono text-xs font-bold" +const emClass = ["text-content font-bold not-italic mr-2", classes?.em] +--- + +
    + { + items.map((item) => ( +
  • + {item.title} + + {item.lead} + {item.text} + +
  • + )) + } +
+ diff --git a/src/components/List/layouts/CardGridList.astro b/src/components/List/layouts/CardGridList.astro new file mode 100644 index 000000000..33297d484 --- /dev/null +++ b/src/components/List/layouts/CardGridList.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + items: { + lead?: string + text: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = ["grid grid-cols-1 md:grid-cols-2 gap-4", classes?.ul] +const liClass = ["bg-page-offset border border-trim rounded-lg p-5 hover:border-primary transition-colors", classes?.li] +const emClass = ["block text-content font-semibold not-italic mb-2", classes?.em] +--- + +
    + { + items.map((item) => ( +
  • + {item.lead} + {item.text} +
  • + )) + } +
diff --git a/src/components/List/layouts/CheckIconsList.astro b/src/components/List/layouts/CheckIconsList.astro new file mode 100644 index 000000000..6f4467665 --- /dev/null +++ b/src/components/List/layouts/CheckIconsList.astro @@ -0,0 +1,46 @@ +--- +import Marker from "@components/List/marker.astro" + +export type Props = { + items: { + lead?: string + text: string + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = ["space-y-3", classes?.ul] +const liClass = ["flex items-start gap-3", classes?.li] +const emClass = ["text-content font-semibold not-italic", classes?.em] +--- + +
    + { + items.map((item) => ( +
  • + + + {item.lead && ( + <> + {item.lead} + + + )} + {item.text} + +
  • + )) + } +
diff --git a/src/components/List/layouts/ChevronList.astro b/src/components/List/layouts/ChevronList.astro new file mode 100644 index 000000000..087334058 --- /dev/null +++ b/src/components/List/layouts/ChevronList.astro @@ -0,0 +1,46 @@ +--- +import Marker from "@components/List/marker.astro" + +export type Props = { + items: { + lead?: string + text: string + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = ["space-y-3", classes?.ul] +const liClass = ["group", classes?.li] +const emClass = ["text-content font-semibold not-italic", classes?.em] +const textClass = "block text-content-offset text-sm mt-0.5" +--- + +
    + { + items.map((item) => ( +
  • +
    + + + +
    + {item.lead} + {item.text} +
    +
    +
  • + )) + } +
diff --git a/src/components/List/layouts/ColoredMarkerList.astro b/src/components/List/layouts/ColoredMarkerList.astro new file mode 100644 index 000000000..2d6f6c7fa --- /dev/null +++ b/src/components/List/layouts/ColoredMarkerList.astro @@ -0,0 +1,40 @@ +--- +export type Props = { + items: { + lead?: string + text: string + color: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } + size?: number +} + +type ColoredMarkerListItem = Props['items'][number] + +const { items, classes, size = 2 }: Props = Astro.props +const coloredItems: ColoredMarkerListItem[] = items.map((item: ColoredMarkerListItem) => ({ + text: item.text, + color: item.color, + ...(item.lead ? { lead: item.lead } : {}), +})) + +const ulClass = ["list-none pl-0 text-content mt-4 mb-2 space-y-2", classes?.ul] +const liClass = ["flex items-start", classes?.li] +const emClass = ["mr-2 not-italic font-bold", classes?.em] +--- + +
    + { + coloredItems.map((item) => ( +
  • + + {item.lead} + {item.text} +
  • + )) + } +
diff --git a/src/components/List/layouts/NumberedWithBackgroundList.astro b/src/components/List/layouts/NumberedWithBackgroundList.astro new file mode 100644 index 000000000..c18a04bff --- /dev/null +++ b/src/components/List/layouts/NumberedWithBackgroundList.astro @@ -0,0 +1,33 @@ +--- +export type Props = { + items: { + lead?: string + text: string + }[] + classes?: { + ol?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const olClass = ["space-y-3", classes?.ol] +const liClass = ["flex items-start gap-4 bg-page-offset/50 rounded-lg p-4", classes?.li] +const emClass = ["text-content font-semibold not-italic", classes?.em] +--- + +
    + { + items.map((item) => ( +
  1. + 1 +
    + {item.lead} +
    {item.text}
    +
    +
  2. + )) + } +
diff --git a/src/components/List/layouts/PlainIconList.astro b/src/components/List/layouts/PlainIconList.astro new file mode 100644 index 000000000..09d9ad786 --- /dev/null +++ b/src/components/List/layouts/PlainIconList.astro @@ -0,0 +1,35 @@ +--- +import Marker from '@components/List/marker.astro' + +export type Props = { + items: { + text: string + icon: string + color: string + }[] + classes?: { + ul?: string + li?: string + svg?: string + } + size?: number +} + +type PlainIconListItem = Props['items'][number] + +const props: Props = Astro.props +const ulClass = ["list-none pl-0 space-y-3 mb-0 mt-2", props.classes?.ul] +const liClass = ["flex items-start gap-3", props.classes?.li] +const markerClasses = props.classes?.svg +--- + +
    + { + props.items.map(({ text, icon, color }: PlainIconListItem) => ( +
  • + + {text} +
  • + )) + } +
\ No newline at end of file diff --git a/src/components/List/layouts/SideBySideList.astro b/src/components/List/layouts/SideBySideList.astro new file mode 100644 index 000000000..4ad86c072 --- /dev/null +++ b/src/components/List/layouts/SideBySideList.astro @@ -0,0 +1,28 @@ +--- +export type Props = { + items: { + lead?: string + text: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const dlClass = ["grid sm:grid-cols-[1fr_2fr] gap-x-8 gap-y-6", classes?.ul] +const dtClass = ["font-bold text-right pt-0.5 border-r border-trim pr-8", classes?.em] +const ddClass = ["text-content-offset", classes?.li] +--- + +
+ { + items.map((item) => ( +
{item.lead}
+
{item.text}
+ )) + } +
diff --git a/src/components/List/layouts/TimelineList.astro b/src/components/List/layouts/TimelineList.astro new file mode 100644 index 000000000..a14d957d9 --- /dev/null +++ b/src/components/List/layouts/TimelineList.astro @@ -0,0 +1,46 @@ +--- +export type Props = { + items: { + lead?: string + text: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +type TimelineItem = Props['items'][number] + +const timelineItems = items.map((item: TimelineItem, index: number) => ({ + ...item, + showConnector: index < items.length - 1, +})) + +const ulClass = ["space-y-0", classes?.ul] +const liClass = ["relative grid grid-cols-[1.5rem_1fr] gap-4", classes?.li] +const emClass = ["block text-content font-bold not-italic mb-1", classes?.em] +const wrapperClass = "mb-4" +const iconClass = ["relative h-5 w-5 mt-0.5 rounded-full border-2 border-primary bg-page-base z-10"] +const connectorClass = ["absolute left-1/2 top-[1.375rem] -bottom-1 -translate-x-1/2 w-0.5 bg-content-offset"] +--- + +
    + { + timelineItems.map((item) => ( +
  • +
    + {item.showConnector && } + +
    +
    + {item.lead} + {item.text} +
    +
  • + )) + } +
diff --git a/src/components/List/layouts/TwoColumnCheckIconsList.astro b/src/components/List/layouts/TwoColumnCheckIconsList.astro new file mode 100644 index 000000000..174cea7bd --- /dev/null +++ b/src/components/List/layouts/TwoColumnCheckIconsList.astro @@ -0,0 +1,38 @@ +--- +import Marker from "@components/List/marker.astro" + +export type Props = { + items: { + text: string + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } + size?: number +} + +const { items, classes, size }: Props = Astro.props + +const ulClass = ["grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-content-offset mb-1", classes?.ul] +const liClass = ["flex items-center gap-2", classes?.li] +--- + +
    + { + items.map((item) => ( +
  • + + + {item.text} + +
  • + )) + } +
diff --git a/src/components/List/layouts/ZebraList.astro b/src/components/List/layouts/ZebraList.astro new file mode 100644 index 000000000..f32b1bc7b --- /dev/null +++ b/src/components/List/layouts/ZebraList.astro @@ -0,0 +1,42 @@ +--- +import Marker from "@components/List/marker.astro" + +export type Props = { + items: { + lead?: string + text: string + color?: string + }[] + classes?: { + ul?: string + li?: string + em?: string + } +} + +const { items, classes }: Props = Astro.props + +const ulClass = ["border border-trim rounded-lg overflow-hidden divide-y divide-trim", classes?.ul] +const liClass = ["p-4 bg-page-base flex items-center justify-between group hover:bg-page-offset transition-colors", classes?.li] +const emClass = ["text-content font-bold not-italic block mb-0.5", classes?.em] +const textClass = ["text-sm text-content-offset"] +--- + +
    + { + items.map((item) => ( +
  • +
    + {item.lead} + {item.text} +
    + +
  • + )) + } +
diff --git a/src/components/List/marker.astro b/src/components/List/marker.astro new file mode 100644 index 000000000..7b282fb81 --- /dev/null +++ b/src/components/List/marker.astro @@ -0,0 +1,26 @@ +--- +import { getMarkerComponent } from '@components/List/server' + +export type Props = { + icon: string + color?: string + size?: number + classes?: string +} + +const { icon, color, size, classes }: Props = Astro.props + +const MarkerComponent = getMarkerComponent(icon) as unknown as (_props: { + color?: string + size?: number + classes?: string +}) => unknown + +const markerProps = { + ...(color ? { color } : {}), + ...(size !== undefined ? { size } : {}), + ...(classes ? { classes } : {}), +} +--- + + diff --git a/src/components/List/markers/avatar.astro b/src/components/List/markers/avatar.astro new file mode 100644 index 000000000..7c3c6048b --- /dev/null +++ b/src/components/List/markers/avatar.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/chart.astro b/src/components/List/markers/chart.astro new file mode 100644 index 000000000..78e876be7 --- /dev/null +++ b/src/components/List/markers/chart.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/check.astro b/src/components/List/markers/check.astro new file mode 100644 index 000000000..2ce6df9e4 --- /dev/null +++ b/src/components/List/markers/check.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + \ No newline at end of file diff --git a/src/components/List/markers/chevron.astro b/src/components/List/markers/chevron.astro new file mode 100644 index 000000000..a3e1bd53e --- /dev/null +++ b/src/components/List/markers/chevron.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/clipboard.astro b/src/components/List/markers/clipboard.astro new file mode 100644 index 000000000..5cfc0c693 --- /dev/null +++ b/src/components/List/markers/clipboard.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/company.astro b/src/components/List/markers/company.astro new file mode 100644 index 000000000..3a40bb1fa --- /dev/null +++ b/src/components/List/markers/company.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/conversation.astro b/src/components/List/markers/conversation.astro new file mode 100644 index 000000000..7248d67da --- /dev/null +++ b/src/components/List/markers/conversation.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/document.astro b/src/components/List/markers/document.astro new file mode 100644 index 000000000..136663bb4 --- /dev/null +++ b/src/components/List/markers/document.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/download-document.astro b/src/components/List/markers/download-document.astro new file mode 100644 index 000000000..4435fbaa5 --- /dev/null +++ b/src/components/List/markers/download-document.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/download.astro b/src/components/List/markers/download.astro new file mode 100644 index 000000000..b059b8199 --- /dev/null +++ b/src/components/List/markers/download.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/email.astro b/src/components/List/markers/email.astro new file mode 100644 index 000000000..dfdc27a5b --- /dev/null +++ b/src/components/List/markers/email.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/eye.astro b/src/components/List/markers/eye.astro new file mode 100644 index 000000000..9a96d7932 --- /dev/null +++ b/src/components/List/markers/eye.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/globe.astro b/src/components/List/markers/globe.astro new file mode 100644 index 000000000..7302af868 --- /dev/null +++ b/src/components/List/markers/globe.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/info-circled.astro b/src/components/List/markers/info-circled.astro new file mode 100644 index 000000000..f59fc69c7 --- /dev/null +++ b/src/components/List/markers/info-circled.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/info.astro b/src/components/List/markers/info.astro new file mode 100644 index 000000000..7c3c6048b --- /dev/null +++ b/src/components/List/markers/info.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/key.astro b/src/components/List/markers/key.astro new file mode 100644 index 000000000..4605063d6 --- /dev/null +++ b/src/components/List/markers/key.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/location.astro b/src/components/List/markers/location.astro new file mode 100644 index 000000000..325f85f46 --- /dev/null +++ b/src/components/List/markers/location.astro @@ -0,0 +1,31 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/lock.astro b/src/components/List/markers/lock.astro new file mode 100644 index 000000000..cfb43b1ca --- /dev/null +++ b/src/components/List/markers/lock.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/paper-and-pen.astro b/src/components/List/markers/paper-and-pen.astro new file mode 100644 index 000000000..e84f7bf25 --- /dev/null +++ b/src/components/List/markers/paper-and-pen.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/paperclip.astro b/src/components/List/markers/paperclip.astro new file mode 100644 index 000000000..08d8fce46 --- /dev/null +++ b/src/components/List/markers/paperclip.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/power.astro b/src/components/List/markers/power.astro new file mode 100644 index 000000000..8f9965d20 --- /dev/null +++ b/src/components/List/markers/power.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/shield.astro b/src/components/List/markers/shield.astro new file mode 100644 index 000000000..cea4d31cb --- /dev/null +++ b/src/components/List/markers/shield.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/trash.astro b/src/components/List/markers/trash.astro new file mode 100644 index 000000000..7760b8f70 --- /dev/null +++ b/src/components/List/markers/trash.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/upload.astro b/src/components/List/markers/upload.astro new file mode 100644 index 000000000..264cc34d5 --- /dev/null +++ b/src/components/List/markers/upload.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/markers/warning.astro b/src/components/List/markers/warning.astro new file mode 100644 index 000000000..3ca81f7cf --- /dev/null +++ b/src/components/List/markers/warning.astro @@ -0,0 +1,25 @@ +--- +export type Props = { + color: string + classes?: string + size: number +} + +const { color, classes, size } = Astro.props +--- + + diff --git a/src/components/List/server/__tests__/index.spec.ts b/src/components/List/server/__tests__/index.spec.ts new file mode 100644 index 000000000..e9c16b983 --- /dev/null +++ b/src/components/List/server/__tests__/index.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import type { AstroComponentFactory } from 'astro/runtime/server/index.js' +import { getMarkerComponent } from '@components/List/server' + +const downloadMarker = (() => null) as unknown as AstroComponentFactory +const avatarMarker = (() => null) as unknown as AstroComponentFactory +const companyMarker = (() => null) as unknown as AstroComponentFactory + +describe('getMarkerComponent', () => { + it('returns marker component when icon exists', () => { + const markerComponents = { + '../markers/download.astro': { default: downloadMarker }, + } + + const markerComponent = getMarkerComponent('download', markerComponents) + + expect(markerComponent).toBe(downloadMarker) + }) + + it('throws a descriptive error when icon is missing', () => { + expect(() => getMarkerComponent(undefined, {})).toThrowError( + 'PlainIconList: missing icon value for list item. Expected an icon name that maps to src/components/List/markers/.astro' + ) + }) + + it('throws a descriptive error when marker file is not found', () => { + const markerComponents = { + '../markers/avatar.astro': { default: avatarMarker }, + '../markers/company.astro': { default: companyMarker }, + } + + expect(() => getMarkerComponent('download', markerComponents)).toThrowError( + 'PlainIconList: marker file "download.astro" was not found in src/components/List/markers/. Requested icon path: "../markers/download.astro". Available markers: avatar.astro, company.astro' + ) + }) +}) diff --git a/src/components/List/server/index.ts b/src/components/List/server/index.ts new file mode 100644 index 000000000..c8af4b173 --- /dev/null +++ b/src/components/List/server/index.ts @@ -0,0 +1,48 @@ +import type { AstroComponentFactory } from 'astro/runtime/server/index.js' + +type MarkerModule = { + default: AstroComponentFactory +} + +export type MarkerComponentMap = Record + +const defaultMarkerComponents = import.meta.glob('../markers/*.astro', { + eager: true, +}) as MarkerComponentMap + +const formatAvailableMarkers = (markerComponents: MarkerComponentMap): string => { + return Object.keys(markerComponents) + .map((path) => path.replace('../markers/', '')) + .sort() + .join(', ') +} + +/** + * Resolve marker component by icon name. + * Throws a descriptive error when icon is missing or marker file is not found. + */ +export const getMarkerComponent = ( + icon?: string, + markerComponents: MarkerComponentMap = defaultMarkerComponents +): AstroComponentFactory => { + if (!icon) { + throw new Error( + 'PlainIconList: missing icon value for list item. Expected an icon name that maps to src/components/List/markers/.astro' + ) + } + + const iconFileName = `${icon}.astro` + const iconPath = `../markers/${iconFileName}` + const markerModule = markerComponents[iconPath] + + if (!markerModule?.default) { + const availableMarkers = formatAvailableMarkers(markerComponents) + + throw new Error( + `PlainIconList: marker file "${iconFileName}" was not found in src/components/List/markers/. ` + + `Requested icon path: "${iconPath}". Available markers: ${availableMarkers || '(none)'}` + ) + } + + return markerModule.default +} diff --git a/src/content/articles/demo/index.mdx b/src/content/articles/demo/index.mdx index 9badd6fb4..f6c9d947a 100644 --- a/src/content/articles/demo/index.mdx +++ b/src/content/articles/demo/index.mdx @@ -1076,6 +1076,675 @@ Nested lists: - First nested list item - Second nested list item +## List Component Layouts + +### Accent Border Left List + +```markdown +
+
+ +
+
+``` + +
+
+ +
+
+ +
+ +### Badge List + + +```markdown + +``` + + + +
+ +### Card Grid List + +```markdown + +``` + + + +
+ +### Check Icons List + +```markdown + +``` + + + +
+ +### Chevron List + +```markdown +
+
+ +
+
+``` + +
+ +
+
+ +
+
+ +### Colored Marker List + +
+ +```markdown +
+
+ +
+
+``` + +
+
+ +
+
+ +
+ +### Numbered With Background List + +```markdown + +``` + +
+ + + +
+ +### Plain Icon List + +```markdown +
+
+ +
+
+``` + +
+
+ +
+
+ +### Side By Side List + +```markdown + +``` + +
+ + + +
+ +### Timeline List + +```markdown + +``` + +
+ + + +
+ +### Two Column Check Icons List + +```markdown + +``` + +
+ + + +
+ +### Zebra List + +```markdown + +``` + +
+ + + +
+ ## Marks Inserts indicated text in a pair of `` elements. Provided by the `remark-mark-plus` plugin. @@ -1352,11 +2021,20 @@ Download PDF files resource="lakehouse-analytics-guide" title="Master Lakehouse Analytics" description="Learn how to build modern data architectures that combine the best of data lakes and warehouses." - buttonText="Download the Guide" - secondaryLink={{ - href: "/services/consulting", - text: "Learn More" - }} + listItems={[ + { + text: "Actionable strategy framework", + }, + { + text: "Expert implementation guidance", + }, + { + text: "Checklist and practical next steps", + }, + { + text: "Download-ready reference material", + }, + ]} /> ``` @@ -1364,14 +2042,23 @@ Download PDF files resource="lakehouse-analytics-guide" title="Master Lakehouse Analytics" description="Learn how to build modern data architectures that combine the best of data lakes and warehouses." - buttonText="Download the Guide" - secondaryLink={{ - href: "/services/consulting", - text: "Learn More" - }} + listItems={[ + { + text: "Actionable strategy framework", + }, + { + text: "Expert implementation guidance", + }, + { + text: "Checklist and practical next steps", + }, + { + text: "Download-ready reference material", + }, + ]} /> -#### Newsletter Signup Component +### Newsletter Signup Component Encourage readers to subscribe to the newsletter. @@ -1390,3 +2077,7 @@ Display customer feedback and testimonials: ``` + +## Footnotes + +Footnotes are automatically added to the bottom of the page. diff --git a/src/content/contact.json b/src/content/contact.json index 03fdb6b12..ae70523ce 100644 --- a/src/content/contact.json +++ b/src/content/contact.json @@ -18,7 +18,7 @@ }, "dataProtectionOfficer": { "name": "Kevin Brown", - "email": "kevin@webstackbuilders.com" + "email": "privacy@webstackbuilders.com" }, "social": [ { diff --git a/src/layouts/MarkdownLayout.astro b/src/layouts/MarkdownLayout.astro index 3d7030df8..3b7032c6c 100644 --- a/src/layouts/MarkdownLayout.astro +++ b/src/layouts/MarkdownLayout.astro @@ -23,6 +23,7 @@ import Download from '@components/CallToAction/Download/index.astro' import Embed from '@components/Social/Embed/index.astro' import Highlighter from '@components/Social/Highlighter/index.astro' import Icon from '@components/Icon/index.astro' +import List from '@components/List/index.astro' import MastodonModal from '@components/Social/Mastodon/index.astro' import Newsletter from '@components/CallToAction/Newsletter/index.astro' import Shares from '@components/Social/Shares/index.astro' @@ -42,6 +43,7 @@ const Components = { Highlighter, Icon, Image, + List, MastodonModal, Newsletter, Picture, diff --git a/src/pages/hero.astro b/src/pages/hero.astro index eda4b310f..733ce26b6 100644 --- a/src/pages/hero.astro +++ b/src/pages/hero.astro @@ -6,447 +6,7 @@ const path = '/hero/' --- - -
-
-

Download CTA Variants

-
-
-
- -
-
- - - Free PDF Guide - -
- -

- Master Lakehouse Analytics -

-

- Learn how to build modern data architectures that combine the best of data lakes and warehouses. -

- -

What you'll learn:

-
    -
  • - - When to choose lakehouse over warehouse -
  • -
  • - - Delta Lake / Iceberg comparison -
  • -
  • - - Cost optimization strategies -
  • -
  • - - Migration playbook + checklist -
  • -
-
- - -
-
-
-
- -
-

24 pages · 5 min read

-
- -
- - - Learn More - -
- -

- No email required. Instant download. -

-
-
-
-
-
-
-
- - -
-

List Style Variations

- - -
-

Variation 1: Accent Border Left

-
    -
  • - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. -
  • -
  • - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. -
  • -
  • - Is a specific consumer affected? - Consumer-specific labels let you slice the data. -
  • -
  • - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. -
  • -
-
- - -
-

Variation 2: Card Grid

-
    -
  • - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. -
  • -
  • - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. -
  • -
  • - Is a specific consumer affected? - Consumer-specific labels let you slice the data. -
  • -
  • - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. -
  • -
-
- - -
-

Variation 3: Check Icons with Dash Separator

-
    -
  • - - - Is the gateway healthy? - - Request rate, error rate, and latency percentiles answer this instantly. - -
  • -
  • - - - Is a specific backend unhealthy? - - Per-upstream metrics and circuit breaker state tell you. - -
  • -
  • - - - Is a specific consumer affected? - - Consumer-specific labels let you slice the data. - -
  • -
  • - - - What happened to this specific request? - - Trace ID and request ID correlation connect all the pieces. - -
  • -
-
- - -
-

Variation 4: Definition List Style

-
-
-
- - Is the gateway healthy? -
-
- Request rate, error rate, and latency percentiles answer this instantly. -
-
-
-
- - Is a specific backend unhealthy? -
-
- Per-upstream metrics and circuit breaker state tell you. -
-
-
-
- - Is a specific consumer affected? -
-
- Consumer-specific labels let you slice the data. -
-
-
-
- - What happened to this specific request? -
-
- Trace ID and request ID correlation connect all the pieces. -
-
-
-
- - -
-

Variation 5: Numbered with Background Accent

-
    -
  1. - 1 -
    - Is the gateway healthy? -

    Request rate, error rate, and latency percentiles answer this instantly.

    -
    -
  2. -
  3. - 2 -
    - Is a specific backend unhealthy? -

    Per-upstream metrics and circuit breaker state tell you.

    -
    -
  4. -
  5. - 3 -
    - Is a specific consumer affected? -

    Consumer-specific labels let you slice the data.

    -
    -
  6. -
  7. - 4 -
    - What happened to this specific request? -

    Trace ID and request ID correlation connect all the pieces.

    -
    -
  8. -
-
- - -
-

Variation 6: Connected Timeline

-
    -
  • - - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. -
  • -
  • - - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. -
  • -
  • - - Is a specific consumer affected? - Consumer-specific labels let you slice the data. -
  • -
  • - - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. -
  • -
-
- - -
-

Variation 7: Tech/Console Badge

-
    -
  • - Query - - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. - -
  • -
  • - Query - - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. - -
  • -
  • - Query - - Is a specific consumer affected? - Consumer-specific labels let you slice the data. - -
  • -
  • - Query - - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. - -
  • -
-
- - -
-

Variation 8: Minimal Chevron

-
    -
  • -
    - - - -
    - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. -
    -
    -
  • -
  • -
    - - - -
    - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. -
    -
    -
  • -
  • -
    - - - -
    - Is a specific consumer affected? - Consumer-specific labels let you slice the data. -
    -
    -
  • -
  • -
    - - - -
    - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. -
    -
    -
  • -
-
- - -
-

Variation 9: Zebra Striped

-
    -
  • -
    - Is the gateway healthy? - Request rate, error rate, and latency percentiles answer this instantly. -
    - -
  • -
  • -
    - Is a specific backend unhealthy? - Per-upstream metrics and circuit breaker state tell you. -
    - -
  • -
  • -
    - Is a specific consumer affected? - Consumer-specific labels let you slice the data. -
    - -
  • -
  • -
    - What happened to this specific request? - Trace ID and request ID correlation connect all the pieces. -
    - -
  • -
-
- - -
-

Variation 10: Side-by-Side Grid

-
-
Is the gateway healthy?
-
Request rate, error rate, and latency percentiles answer this instantly.
- -
Is a specific backend unhealthy?
-
Per-upstream metrics and circuit breaker state tell you.
- -
Is a specific consumer affected?
-
Consumer-specific labels let you slice the data.
- -
What happened to this specific request?
-
Trace ID and request ID correlation connect all the pieces.
-
-
-
+
diff --git a/src/pages/privacy/index.astro b/src/pages/privacy/index.astro index b05a67547..158164483 100644 --- a/src/pages/privacy/index.astro +++ b/src/pages/privacy/index.astro @@ -1,866 +1,356 @@ --- import BaseLayout from '@layouts/BaseLayout.astro' -import contactData from '@content/contact.json' +import PrivacyLayout from '@components/Layout/Privacy/index.astro' const pageTitle = 'Privacy Policy' const path = '/privacy' const description = 'Learn how Webstack Builders collects, uses, and protects your personal information.' -const lastUpdatedDate = new Date() -const lastUpdatedIso = lastUpdatedDate.toISOString() -const lastUpdatedText = lastUpdatedDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', -}) --- -
-
-

Privacy Policy

-

- Your privacy is important to us. This policy explains how we collect, use, and protect your - information. -

-
- -
-

- Last updated: -

- -
-

- Information We Collect -

-
-

Personal Information

-

- When you contact us through our website, we may collect: -

-
    -
  • Your name and email address
  • -
  • Company name and contact information
  • -
  • Project details and requirements you share with us
  • -
  • Any files or documents you upload through our contact form
  • -
-
- -
-

Automatically Collected Information

-

- We automatically collect certain information when you visit our website: -

-
    -
  • Browser type and version (user agent)
  • -
  • Pages visited and time spent on our site
  • -
  • Referring website information
  • -
-

- Note: We do not collect or store IP addresses. -

-
-
- -
-

- How We Use Your Information -

-
-
-
-
- -
-

Communication

-
-

- To respond to your inquiries, provide project quotes, and communicate about potential - services. -

-
- -
-
-
- -
-

Analytics

-
-

- To analyze website usage patterns and improve our site's functionality and user - experience. -

-
- -
-
-
- -
-

Security

-
-

- To protect our website and services from fraud, abuse, and unauthorized access. -

-
- -
-
-
- -
-

Legal Compliance

-
-

- To comply with applicable laws, regulations, and legal processes. -

-
-
-
- -
-

- Information Sharing -

-
-
-
- -
-
-

- We Do Not Sell Your Information -

-

- We do not sell, trade, or otherwise transfer your personal information to third - parties. We may share information only in the following limited circumstances: -

-
    -
  • With trusted service providers who assist in operating our website
  • -
  • When required by law or to protect our legal rights
  • -
  • In connection with a business transfer or merger
  • -
  • With your explicit consent
  • -
-
-
-
-
- -
-

- Data Security -

-

- We implement appropriate technical and organizational security measures to protect your - personal information against unauthorized access, alteration, disclosure, or destruction. -

-
-
-
- -
-

SSL Encryption

-

- All data transmitted between your browser and our servers is encrypted -

-
-
-
- -
-

- Access Controls -

-

- Limited access to personal information on a need-to-know basis -

-
-
-
- -
-

- Regular Updates -

-

- Regular security audits and updates to protect against threats -

-
-
-
- -
-

- Your Rights -

-
-

- You have the following rights regarding your personal information: -

-
-
-
- -
-
-

Access

-

- Request a copy of the personal information we hold about you -

-
-
-
-
- -
-
-

Correction

-

- Request correction of inaccurate or incomplete information -

-
-
-
-
- -
-
-

Deletion

-

- Request deletion of your personal information -

-
-
-
-
- -
-
-

Opt-out

-

- Withdraw consent for processing your information -

-
-
-
-
-
- -
-

- Cookies and Tracking -

-

- Our website uses cookies and similar tracking technologies to enhance your browsing - experience. You can control cookie settings through your browser preferences. -

-
-

- Types of Cookies We Use: -

-
    -
  • - -
    - Essential Cookies: - - Required for basic website functionality -
    -
  • -
  • - -
    - Analytics Cookies: - - Help us understand how visitors use our website -
    -
  • -
  • - -
    - Preference Cookies: - - Remember your settings and preferences -
    -
  • -
-
-
- -
-

- Strictly Necessary Storage -

-

- In addition to cookies, we use browser localStorage to store certain information that is - essential for our website to function properly and provide you with a better user - experience. Under GDPR Article 6(1)(f), we process this data based on our legitimate - interest to provide a functional and accessible website. You retain the right to opt out - of optional features, though this may impact your experience. -

-
-

- What We Store Locally: -

-
-
-

- Cookie Consent Preferences -

-

- We remember your cookie consent choices (analytics, marketing, functional) so we - don't repeatedly ask you. This prevents consent banner fatigue and respects your - initial decision. -

-
-
-

- Theme Preference (Accessibility Feature) -

-

- Your selected visual theme (light, dark, holiday, or A11y high-contrast) is stored - to maintain visual consistency across pages and visits. This prevents jarring theme - changes during navigation and ensures accessibility for users with visual - impairments. -

-

- Accessibility Justification: Our A11y theme meets WCAG 1.4.6 Level AAA - standards (Contrast Enhanced), requiring a contrast ratio of at least 7:1 for normal text. - Users with low vision or color blindness depend on this high-contrast theme for legible - content. Storing the theme preference is necessary to deliver an accessible experience - consistently across your browsing session, as required by accessibility regulations including - the European Accessibility Act and WCAG guidelines. This constitutes a legitimate interest - under GDPR Article 6(1)(f) and requires no consent. -

-
-
-

- Social Media Embed Cache -

-

- When you load social media embeds (Twitter, YouTube, etc.), we cache the public API - responses locally to reduce repeated network requests. This improves page load times - and reduces bandwidth usage for both you and the third-party services. No personal - data is stored—only publicly available embed content. -

-
-
-

- Mastodon Instances (Requires Consent) -

-

- If you interact with Mastodon social sharing features and provide consent, we - remember your preferred Mastodon instance(s) to streamline future interactions. We - store up to 5 instances using a first-in-first-out approach. This feature requires explicit opt-in consent because your Mastodon instance choice can identify you and constitutes personal data - under GDPR Article 4(1). -

-
-
-

- Error Logging (Sentry) -

-

- We use Sentry for error monitoring to identify and fix bugs quickly. Sentry may - store session information locally to track errors across page loads. We configure - Sentry to avoid collecting personally identifiable information (no emails, - usernames, or IP addresses). -

-
-
-
-

- Your Choice: Theme preference, consent tracking, social embed caching, - and error monitoring are classified as strictly necessary for providing a functional and - accessible website. These features do not require consent. Optional features like Mastodon - instance storage require opt-in consent through our Cookie Preference Center. Disabling - optional storage will prevent the website from remembering your Mastodon preferences but - will not affect core functionality or accessibility features. -

-
-
-
- -
-

- Error Monitoring and Diagnostics -

-

- We use Sentry, a third-party error monitoring service, to help us identify and fix - technical issues on our website. This helps us maintain a high-quality user experience and - quickly resolve bugs. -

- -
-

- What We Collect for Error Monitoring: -

-
    -
  • - -
    - Browser Information: - - User agent (browser type and version) to help diagnose browser-specific issues - -
    -
  • -
  • - -
    - Error Details: - - Technical error messages, stack traces, and the sequence of actions leading to - errors - -
    -
  • -
  • - -
    - Page Information: - - URLs visited and navigation history when errors occur - -
    -
  • -
-
- -
-

- - Privacy Protections: -

-
    -
  • - - No IP addresses collected or stored -
  • -
  • - - No session replay or screen recording -
  • -
  • - - No performance tracking or monitoring -
  • -
  • - - Data is used solely for technical troubleshooting and bug fixes -
  • -
-
- -
-

- Data Processing Agreement -

-

- Webstack Builders has signed a Data Processing Agreement (DPA) with Sentry to ensure - that all error monitoring data is processed in compliance with GDPR and other data - protection regulations. This agreement ensures that Sentry acts as a data processor on - our behalf and implements appropriate security measures to protect your information. -

-
-
- -
-

- Contact Us -

-
-

- If you have any questions about this Privacy Policy or would like to exercise your - rights, please contact us: -

-
-
-
- -
-
-

Email

- - Contact Us - -
-
-
-
- -
-
-

- Response Time -

-

- We respond to privacy requests within 30 days -

-
-
-
- -
-

- Data Protection Officer -

-

- For questions specifically related to data protection and privacy compliance: -

-
-

- {contactData.company.dataProtectionOfficer.name} -

- - {contactData.company.dataProtectionOfficer.email} - -
-
-
-
- -
-

- Changes to This Policy -

-
-

- We may update this Privacy Policy from time to time to reflect changes in our practices - or applicable laws. We will notify you of any material changes by posting the updated - policy on this page and updating the "Last updated" date at the top of this policy. We - encourage you to review this policy periodically. -

-
-
-
-
+ Accessibility Justification: Our A11y theme meets WCAG 1.4.6 Level AAA standards (Contrast Enhanced), requiring a contrast ratio of at least 7:1 for normal text. Users with low vision or color blindness depend on this high-contrast theme for legible content. Storing the theme preference is necessary to deliver an accessible experience consistently across your browsing session, as required by accessibility regulations including the European Accessibility Act and WCAG guidelines. This constitutes a legitimate interest under GDPR Article 6(1)(f) and requires no consent.", + ], + color: "border-purple-500", + }, + { + lead: "Social Media Embed Cache", + text: "When you load social media embeds (Twitter, YouTube, etc.), we cache the public API responses locally to reduce repeated network requests. This improves page load times and reduces bandwidth usage for both you and the third-party services. No personal data is stored—only publicly available embed content.", + color: "border-green-500", + }, + { + lead: "Mastodon Instances (Requires Consent)", + text: "If you interact with Mastodon social sharing features and provide consent, we remember your preferred Mastodon instance(s) to streamline future interactions. We store up to 5 instances using a first-in-first-out approach. This feature requires explicit opt-in consent because your Mastodon instance choice can identify you and constitutes personal data under GDPR Article 4(1).", + color: "border-orange-500", + }, + { + lead: "Error Logging (Sentry)", + text: "We use Sentry for error monitoring to identify and fix bugs quickly. Sentry may store session information locally to track errors across page loads. We configure Sentry to avoid collecting personally identifiable information (no emails, usernames, or IP addresses).", + color: "border-red-500", + }, + ], + trailer: "Your Choice: Theme preference, consent tracking, social embed caching, and error monitoring are classified as strictly necessary for providing a functional and accessible website. These features do not require consent. Optional features like Mastodon instance storage require opt-in consent through our Cookie Preference Center. Disabling optional storage will prevent the website from remembering your Mastodon preferences but will not affect core functionality or accessibility features.", + }, + monitoring: { + title: "Error Monitoring and Diagnostics", + description: "We use Sentry, a third-party error monitoring service, to help us identify and fix technical issues on our website. This helps us maintain a high-quality user experience and quickly resolve bugs.", + list1: { + lead: "What We Collect for Error Monitoring:", + listItems: [ + { + heading: "Browser Information:", + text: "User agent (browser type and version) to help diagnose browser-specific issues", + }, + { + heading: "Error Details:", + text: "Technical error messages, stack traces, and the sequence of actions leading to errors", + }, + { + heading: "Page Information:", + text: "URLs visited and navigation history when errors occur", + }, + ], + }, + list2: { + lead: "Privacy Protections:", + listItems: [ + { + text: "No IP addresses collected or stored", + }, + { + text: "No session replay or screen recording", + }, + { + text: "No performance tracking or monitoring", + }, + { + text: "Data is used solely for technical troubleshooting and bug fixes", + }, + ], + }, + close: { + heading: "Data Processing Agreement", + text: "Webstack Builders has signed a Data Processing Agreement (DPA) with Sentry to ensure that all error monitoring data is processed in compliance with GDPR and other data protection regulations. This agreement ensures that Sentry acts as a data processor on our behalf and implements appropriate security measures to protect your information.", + } + }, + contact: { + title: "Contact Us", + description: "If you have any questions about this Privacy Policy or would like to exercise your rights, please contact us:", + card1: { + lead: "Contact Us", + text: "Email", + link: "/contact/", + }, + card2: { + lead: "Response Time", + text: "We respond to privacy requests within 30 days", + }, + card3: { + lead: "Data Protection Officer", + text: "For questions specifically related to data protection and privacy compliance:", + }, + }, + changes: { + title: "Changes to This Policy", + description: "We may update this Privacy Policy from time to time to reflect changes in our practices or applicable laws. We will notify you of any material changes by posting the updated policy on this page and updating the \"Last updated\" date at the top of this policy. We encourage you to review this policy periodically.", + }, + }} + />