Skip to content

Commit 388d8de

Browse files
committed
Update styling in List and Tables for platform-architecture-control-plane-data-plane-separation article
1 parent ba2c8a3 commit 388d8de

10 files changed

Lines changed: 282 additions & 101 deletions

File tree

‎_TODO.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,6 @@ https://mermaid.js.org/config/directives.html
226226

227227
- Improve `<abbr>` styling: https://codepen.io/ire/pen/NoqWpm
228228

229-
- Moving the scroll bar up quickly with the mouse seems to make the header logic break - the Switcher component and Breadcrumbs are hidden under the header
230-
231229
## List Component
232230

233231
- Task list checked variant Markdown in dark theme is awkward, it has a dark shadow
@@ -240,6 +238,8 @@ https://mermaid.js.org/config/directives.html
240238

241239
- 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.
242240

241+
- Moving the scroll bar up quickly with the mouse seems to make the header logic break - the Switcher component and Breadcrumbs are hidden under the header
242+
243243
## Content Issues
244244

245245
- Need an article on OpenStack
@@ -252,7 +252,7 @@ https://mermaid.js.org/config/directives.html
252252

253253
- Add a "Preview Special" item to our Download CTA that lets the user know the Deep Dive content can be previewed in HTML format, and offer a switch to it.
254254

255-
- How can we handle footnotes in List components? src/content/articles/kubernetes-multi-cluster-fleet-management-configuration/pdf.mdx line 80
255+
- Tags should break more evenly across two lines when there's a lot of them, instead of forcing the author name and date to break across two lines: platform-engineering-metrics-lead-time-developer-friction/index.mdx
256256

257257
- Need to update Case Studies with lists and tables too
258258

‎src/components/List/ListItem.astro‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
export type Props = {
3+
lead?: string
4+
}
5+
6+
const { lead } = Astro.props as Props
7+
---
8+
9+
<wsb-list-item {...(lead ? { 'data-lead': lead } : {})}><slot /></wsb-list-item>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
import List from '@components/List/index.astro'
3+
import ListItem from '@components/List/ListItem.astro'
4+
---
5+
6+
<List
7+
variant="chevron-list"
8+
items={[
9+
{
10+
lead: 'Prop item',
11+
text: 'This should fail because slot items are also provided.',
12+
},
13+
]}
14+
>
15+
<ListItem lead="Slot item">This should never render.</ListItem>
16+
</List>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
import List from '@components/List/index.astro'
3+
import ListItem from '@components/List/ListItem.astro'
4+
---
5+
6+
<List variant="chevron-list">
7+
<ListItem lead="Network isolation">
8+
Separate VPCs<sup data-footnote-ref="slot-demo">1</sup> can provide stronger boundaries.
9+
</ListItem>
10+
<ListItem lead="Compute isolation">
11+
Dedicated node pools reduce resource contention.
12+
</ListItem>
13+
</List>

‎src/components/List/__tests__/index.spec.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { beforeEach, describe, expect, test } from 'vitest'
22
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
33
import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
4+
import MixedApiFixture from '@components/List/__fixtures__/mixedApi.fixture.astro'
5+
import RichItemsFixture from '@components/List/__fixtures__/richItems.fixture.astro'
46

57
describe('List (Astro)', () => {
68
let container: AstroContainer
@@ -232,4 +234,25 @@ describe('List (Astro)', () => {
232234
expect(title?.textContent).toContain('Resource metrics')
233235
})
234236
})
237+
238+
test('renders rich ListItem children through the existing layout item API', async () => {
239+
const renderedHtml = await container.renderToString(RichItemsFixture)
240+
241+
await withJsdomEnvironment(async ({ window }) => {
242+
window.document.body.innerHTML = renderedHtml
243+
244+
const items = Array.from(window.document.querySelectorAll('ul > li'))
245+
expect(items).toHaveLength(2)
246+
expect(items[0]?.querySelector('em')?.textContent).toContain('Network isolation')
247+
expect(items[0]?.querySelector('sup[data-footnote-ref="slot-demo"]')?.textContent).toBe('1')
248+
expect(items[1]?.textContent).toContain('Dedicated node pools reduce resource contention.')
249+
expect(window.document.querySelector('wsb-list-item')).toBeNull()
250+
})
251+
})
252+
253+
test('throws a BuildError when items and ListItem children are both provided', async () => {
254+
await expect(container.renderToString(MixedApiFixture)).rejects.toThrow(
255+
'List: received both the `items` prop and ListItem children. Use one API or the other.'
256+
)
257+
})
235258
})

‎src/components/List/index.astro‎

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
import { BuildError } from '@lib/errors/BuildError'
23
import AccentBorderLeftList from '@components/List/layouts/AccentBorderLeftList.astro'
34
import BadgeList from '@components/List/layouts/BadgeList.astro'
45
import CardGridList from '@components/List/layouts/CardGridList.astro'
@@ -14,9 +15,10 @@ import ThreeColumnIconList from '@components/List/layouts/ThreeColumnIconList.as
1415
import TwoColumnCheckIconsList from '@components/List/layouts/TwoColumnCheckIconsList.astro'
1516
import TwoColumnIconList from '@components/List/layouts/TwoColumnIconList.astro'
1617
import ZebraList from '@components/List/layouts/ZebraList.astro'
18+
import { getListItemsFromSlotMarkup } from '@components/List/server/slotItems'
1719
1820
export type Props = {
19-
items: {
21+
items?: {
2022
title?: string
2123
lead?: string
2224
text: string
@@ -45,14 +47,33 @@ export type Props = {
4547
style?: Record<string, unknown>
4648
}
4749
48-
const { items, size, color, startNumber, variant = 'default', classes, style } = Astro.props
50+
const { items, size, color, startNumber, variant = 'default', classes, style } = Astro.props as Props
51+
const hasItemsProp = Object.prototype.hasOwnProperty.call(Astro.props, 'items')
52+
const hasDefaultSlot = Astro.slots.has('default')
53+
54+
if (hasItemsProp && hasDefaultSlot) {
55+
throw new BuildError(
56+
'List: received both the `items` prop and ListItem children. Use one API or the other.',
57+
{
58+
phase: 'compilation',
59+
filePath: 'src/components/List/index.astro',
60+
tool: 'list',
61+
}
62+
)
63+
}
64+
65+
const slotMarkup = hasDefaultSlot ? await Astro.slots.render('default') : ''
66+
const normalizedItems = hasDefaultSlot
67+
? getListItemsFromSlotMarkup(slotMarkup, variant)
68+
: (items ?? [])
69+
4970
const classesProps = classes ? { classes } : {}
5071
const numericSizeProps = typeof size === 'number' ? { size } : {}
5172
const chatBubbleSizeProps = typeof size === 'string' ? { size } : {}
5273
const colorProps = typeof color === 'string' && color.trim().length > 0 ? { color } : {}
5374
const startNumberProps = typeof startNumber === 'number' ? { startNumber } : {}
5475
const styleProps = style ? { style } : {}
55-
const itemsWithColor = items as Array<{
76+
const itemsWithColor = normalizedItems as Array<{
5677
title?: string
5778
lead?: string
5879
text: string
@@ -61,32 +82,32 @@ const itemsWithColor = items as Array<{
6182
inverseColor?: string
6283
bgColor?: string
6384
}>
64-
const itemsWithIconAndColor = items as Array<{
85+
const itemsWithIconAndColor = normalizedItems as Array<{
6586
text: string
6687
icon: string
6788
color: string
6889
inverseColor?: string
6990
bgColor?: string
7091
}>
71-
const plainIconItems = items.filter((item): item is Props['items'][number] & { icon: string } => {
92+
const plainIconItems = normalizedItems.filter((item): item is NonNullable<Props['items']>[number] & { icon: string } => {
7293
return typeof item.icon === 'string' && item.icon.trim().length > 0
7394
})
7495
---
7596

7697
<div class:list={[classes?.wrapper ? classes.wrapper : 'mb-6']}>
77-
{variant === 'accent-border-left-list' && <AccentBorderLeftList items={items} {...classesProps} {...styleProps} />}
78-
{variant === 'badge-list' && <BadgeList items={items} {...classesProps} {...styleProps} />}
79-
{variant === 'card-grid-list' && <CardGridList items={items} {...classesProps} {...styleProps} />}
80-
{variant === 'chat-bubbles' && <ChatBubbles items={items} {...classesProps} {...chatBubbleSizeProps} />}
81-
{variant === 'check-icons-list' && <CheckIconsList items={items} {...classesProps} {...styleProps} />}
82-
{variant === 'chevron-list' && <ChevronList items={items} {...classesProps} {...styleProps} />}
98+
{variant === 'accent-border-left-list' && <AccentBorderLeftList items={normalizedItems} {...classesProps} {...styleProps} />}
99+
{variant === 'badge-list' && <BadgeList items={normalizedItems} {...classesProps} {...styleProps} />}
100+
{variant === 'card-grid-list' && <CardGridList items={normalizedItems} {...classesProps} {...styleProps} />}
101+
{variant === 'chat-bubbles' && <ChatBubbles items={normalizedItems} {...classesProps} {...chatBubbleSizeProps} />}
102+
{variant === 'check-icons-list' && <CheckIconsList items={normalizedItems} {...classesProps} {...styleProps} />}
103+
{variant === 'chevron-list' && <ChevronList items={normalizedItems} {...classesProps} {...styleProps} />}
83104
{variant === 'colored-marker-list' && <ColoredMarkerList items={itemsWithColor} {...classesProps} {...numericSizeProps} {...styleProps} />}
84-
{variant === 'numbered-with-background-list' && <NumberedWithBackgroundList items={items} {...classesProps} {...colorProps} {...startNumberProps} {...styleProps} />}
105+
{variant === 'numbered-with-background-list' && <NumberedWithBackgroundList items={normalizedItems} {...classesProps} {...colorProps} {...startNumberProps} {...styleProps} />}
85106
{variant === 'plain-icon-list' && <PlainIconList items={plainIconItems} {...classesProps} {...numericSizeProps} {...styleProps} />}
86-
{variant === 'side-by-side-list' && <SideBySideList items={items} {...classesProps} {...styleProps} />}
87-
{variant === 'timeline-list' && <TimelineList items={items} {...classesProps} {...styleProps} />}
88-
{variant === 'two-column-check-icons-list' && <TwoColumnCheckIconsList items={items} {...classesProps} {...numericSizeProps} {...styleProps} />}
107+
{variant === 'side-by-side-list' && <SideBySideList items={normalizedItems} {...classesProps} {...styleProps} />}
108+
{variant === 'timeline-list' && <TimelineList items={normalizedItems} {...classesProps} {...styleProps} />}
109+
{variant === 'two-column-check-icons-list' && <TwoColumnCheckIconsList items={normalizedItems} {...classesProps} {...numericSizeProps} {...styleProps} />}
89110
{variant === 'two-column-icon-list' && <TwoColumnIconList items={itemsWithIconAndColor} {...classesProps} {...numericSizeProps} {...styleProps} />}
90111
{variant === 'three-column-icon-list' && <ThreeColumnIconList items={itemsWithIconAndColor} {...classesProps} {...numericSizeProps} {...styleProps} />}
91-
{variant === 'zebra-list' && <ZebraList items={items} {...classesProps} {...styleProps} />}
112+
{variant === 'zebra-list' && <ZebraList items={normalizedItems} {...classesProps} {...styleProps} />}
92113
</div>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { JSDOM } from 'jsdom'
2+
import { BuildError } from '@lib/errors/BuildError'
3+
import type { Props as ListProps } from '@components/List/index.astro'
4+
5+
type ListItemShape = NonNullable<ListProps['items']>[number]
6+
7+
const unsupportedRichSlotVariants = new Set([
8+
'plain-icon-list',
9+
'two-column-icon-list',
10+
'three-column-icon-list',
11+
])
12+
13+
const buildErrorContext = {
14+
phase: 'compilation',
15+
filePath: 'src/components/List/index.astro',
16+
tool: 'list',
17+
} as const
18+
19+
/**
20+
* Parse rendered ListItem slot markup into the existing List items shape.
21+
*/
22+
export function getListItemsFromSlotMarkup(markup: string, variant: string): ListItemShape[] {
23+
if (unsupportedRichSlotVariants.has(variant)) {
24+
throw new BuildError(
25+
`List: rich ListItem children are not supported for the \`${variant}\` variant. Use the \`items\` prop for icon-based list variants.`,
26+
buildErrorContext
27+
)
28+
}
29+
30+
const document = new JSDOM(`<!doctype html><html><body>${markup}</body></html>`).window.document
31+
const meaningfulNodes = Array.from(document.body.childNodes).filter((node) => {
32+
if (node.nodeType === node.TEXT_NODE) {
33+
return node.textContent?.trim().length
34+
}
35+
36+
return node.nodeType === node.ELEMENT_NODE
37+
})
38+
39+
if (meaningfulNodes.length === 0) {
40+
throw new BuildError(
41+
'List: expected one or more ListItem children when using rich slot content.',
42+
buildErrorContext
43+
)
44+
}
45+
46+
const invalidNode = meaningfulNodes.find((node) => {
47+
return node.nodeType !== node.ELEMENT_NODE || (node as Element).tagName.toLowerCase() !== 'wsb-list-item'
48+
})
49+
50+
if (invalidNode) {
51+
throw new BuildError(
52+
'List: rich slot content must contain only ListItem children. Use either the `items` prop or `<ListItem>` children.',
53+
buildErrorContext
54+
)
55+
}
56+
57+
return meaningfulNodes.map((node) => {
58+
const element = node as Element
59+
60+
return {
61+
lead: element.getAttribute('data-lead') ?? undefined,
62+
text: element.innerHTML.trim(),
63+
}
64+
})
65+
}

‎src/content/articles/kubernetes-multi-cluster-fleet-management-configuration/pdf.mdx‎

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,32 @@ Organizations arrive at multi-cluster architectures for different reasons, and t
7878

7979
Every multi-cluster strategy sits somewhere on a spectrum from "all clusters are identical" to "each cluster is independent." Neither extreme works well in practice.
8080

81-
<List
82-
variant="numbered-with-background-list"
83-
items={[
84-
{
85-
lead: "Fully consistent",
86-
text: "means all clusters get exactly the same configuration. This is simple to reason about and guarantees reproducibility, but it's inflexible. You end up over-provisioning dev and staging to match production, or you can't tune production for its actual load patterns. This approach only works for small fleets with simple applications.",
87-
},
88-
{
89-
lead: "Layered configuration",
90-
text: `uses a base configuration with environment-specific overrides. This is the sweet spot for most organizations. You define what's shared (security policies, monitoring stack, core add-ons) and what varies (replica counts, resource limits, feature flags). Kustomize overlays and Helm values files are the typical implementation. The risk is "override explosion"—so many layers of patches that nobody can trace what a cluster actually runs.`,
91-
},
92-
{
93-
lead: "Policy-governed",
94-
text: "configuration sets guardrails rather than identical configs. Teams can customize their clusters within bounds enforced by OPA/Gatekeeper[^opa-gatekeeper] or Kyverno[^kyverno]. This enables innovation and team autonomy, but drift happens within the policy bounds. Debugging becomes harder because clusters are intentionally different.",
95-
},
96-
{
97-
lead: "Fully flexible",
98-
text: "means each cluster is independent, with its own GitOps repo and configuration. Maximum autonomy, maximum chaos. Security risks emerge when teams deviate from baselines. This only works with truly autonomous teams who don't need cross-cluster consistency.",
99-
},
100-
]}
101-
/>
81+
<List variant="numbered-with-background-list">
82+
<ListItem lead="Fully consistent">
83+
Means all clusters get exactly the same configuration. This is simple to reason about and guarantees
84+
reproducibility, but it's inflexible. You end up over-provisioning dev and staging to match
85+
production, or you can't tune production for its actual load patterns. This approach only works for
86+
small fleets with simple applications.
87+
</ListItem>
88+
<ListItem lead="Layered configuration">
89+
Uses a base configuration with environment-specific overrides. This is the sweet spot for most
90+
organizations. You define what's shared (security policies, monitoring stack, core add-ons) and
91+
what varies (replica counts, resource limits, feature flags). Kustomize overlays and Helm values
92+
files are the typical implementation. The risk is "override explosion"—so many layers of patches
93+
that nobody can trace what a cluster actually runs.
94+
</ListItem>
95+
<ListItem lead="Policy-governed">
96+
Configuration sets guardrails rather than identical configs. Teams can customize their clusters
97+
within bounds enforced by OPA/Gatekeeper[^opa-gatekeeper] or Kyverno[^kyverno]. This enables
98+
innovation and team autonomy, but drift happens within the policy bounds. Debugging becomes harder
99+
because clusters are intentionally different.
100+
</ListItem>
101+
<ListItem lead="Fully flexible">
102+
Means each cluster is independent, with its own GitOps repo and configuration. Maximum autonomy,
103+
maximum chaos. Security risks emerge when teams deviate from baselines. This only works with truly
104+
autonomous teams who don't need cross-cluster consistency.
105+
</ListItem>
106+
</List>
102107

103108
[^opa-gatekeeper]: OPA (Open Policy Agent) is an open-source, general-purpose policy engine that unifies policy enforcement across the stack, using a declarative language called Rego to define complex rules. Gatekeeper is a specialized project that integrates OPA into Kubernetes — it acts as a validating admission controller, intercepting requests to the Kubernetes API and checking them against OPA policies before resources are created or modified.
104109

0 commit comments

Comments
 (0)