Skip to content

Commit 361a05e

Browse files
committed
Add line wrap in code blocks
1 parent ff5148b commit 361a05e

11 files changed

Lines changed: 237 additions & 54 deletions

File tree

‎_TODO.md‎

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ Cons: Browser rendering can be inconsistent across different operating systems a
2121

2222
Axe accessibility (2) - axe-core integration
2323

24-
## Search UI
25-
26-
- Move search box into header. Should be an icon like theme picker and hamburger menu, and spread out when clicked.
27-
- On search results page, center results on page
28-
- Improve indexing and how contents are returned.
29-
3024
## Performance
3125

3226
Implement mitigations in test/e2e/specs/07-performance/PERFORMANCE.md
@@ -41,7 +35,6 @@ Vercel AI Gateway, maybe could use for a chatbot:
4135

4236
https://vercel.com/kevin-browns-projects-dd474f73/astro-webstackbuilders-com/ai-gateway
4337

44-
4538
## Testimonials on mobile
4639

4740
We have E2E errors again testimonials slide on mobile chrome and safari. I think the problem is that we are pausing carousels when part of the carousel is outside of the viewport, and the testimonials are too large to display on mobile without being off viewport.
@@ -52,10 +45,6 @@ We have E2E errors again testimonials slide on mobile chrome and safari. I think
5245

5346
We should start the mock containers with the dev server instead of with Playwright so that they're useable in a dev environment.
5447

55-
## Add Tooltip component
56-
57-
We need a Tooltip component. It should apply to the existing tooltips on the theme picker palettes.
58-
5948
## Improve print layout by hiding header and footer for articles, add tracking
6049

6150
```typescript
@@ -114,8 +103,14 @@ Link to the second-order category from the "Skills/Technologies Preview" icons o
114103

115104
- Show article titles only in drop-down search result box, and dedupe results
116105

106+
## Header
107+
117108
Title box - need to squish to 75% and have it absolute in place as you scroll down, go back to 100% when you scroll back up
118109

110+
## Themepicker
111+
112+
Need to tweak 1px border for non-active theme cards
113+
119114
## ToolTips
120115

121116
Need a tooltip component for consistency. List to add tooltips to:
@@ -182,13 +177,3 @@ We have generated detailed outlines for each of the MDX proposed articles we hav
182177
Act as a principal software engineer. Your goal is to write a detailed technical article based on the provided outline. Context: The target audience is Senior DevOps and infrastructure Engineers. The tone should be authoritative, professional, and concise, avoiding fluff or filler words. Think step-by-step before writing to improve the accuracy of technical explanations. Use a friendly first-person voice. Anything that reads like generic marketing copy is not what we want but we still want the effect of being "real" and approachable - try not to sound like technical documentation. We want to show empathy for our readers.
183178

184179
Let's work through each article section by section based on the H2 headers in the outline. If the section looks good as-is, I'll just type "ok" so you know to continue to the next section.
185-
186-
## Link Check
187-
188-
Should we disable external link checking? Or maybe require allow-listing them?
189-
190-
## Carousel
191-
192-
- On hover when it rises, the corners get cut off and squared. It should raise to a higher index and move up.
193-
- The focus outline should just replace the existing border with the highlight border.
194-
- The card background isn't distinct enough - maybe it needs shadow all the way around, or a darker color.

‎src/components/Code/CodeTabs/client/__tests__/index.spec.ts‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,89 @@ describe('CodeTabs web component', () => {
102102
expect(() => copyButton?.click()).not.toThrow()
103103
})
104104
})
105+
106+
it('renders a single tab label for a single named code block', async () => {
107+
await withJsdomEnvironment(async ({ window }) => {
108+
const { registerCodeTabsWebComponent } =
109+
await import('@components/Code/CodeTabs/client/index')
110+
registerCodeTabsWebComponent()
111+
112+
const element = window.document.createElement('code-tabs')
113+
const pre = window.document.createElement('pre')
114+
pre.setAttribute('data-language', 'typescript')
115+
116+
const code = window.document.createElement('code')
117+
code.textContent = 'const x: number = 1'
118+
pre.appendChild(code)
119+
120+
element.appendChild(pre)
121+
window.document.body.appendChild(element)
122+
123+
const tabButton = element.querySelector('button[data-code-tabs-button="0"]')
124+
expect(tabButton, 'CodeTabs should render a tab button for a single named code block').toBeTruthy()
125+
expect(tabButton?.textContent).toBe('TypeScript')
126+
expect(
127+
tabButton?.className.includes('hover:text-gray-600'),
128+
'Single-tab UI should not change text color on hover'
129+
).toBe(false)
130+
})
131+
})
132+
133+
it('does not apply hover color change to the active tab when multiple tabs exist', async () => {
134+
await withJsdomEnvironment(async ({ window }) => {
135+
const { registerCodeTabsWebComponent } =
136+
await import('@components/Code/CodeTabs/client/index')
137+
registerCodeTabsWebComponent()
138+
139+
const element = window.document.createElement('code-tabs')
140+
141+
const preA = window.document.createElement('pre')
142+
preA.setAttribute('data-code-tabs-tab', 'A')
143+
preA.setAttribute('data-language', 'javascript')
144+
preA.appendChild(window.document.createElement('code'))
145+
146+
const preB = window.document.createElement('pre')
147+
preB.setAttribute('data-code-tabs-tab', 'B')
148+
preB.setAttribute('data-language', 'typescript')
149+
preB.appendChild(window.document.createElement('code'))
150+
151+
element.appendChild(preA)
152+
element.appendChild(preB)
153+
window.document.body.appendChild(element)
154+
155+
const activeButton = element.querySelector('button[data-code-tabs-button="0"]')
156+
const inactiveButton = element.querySelector('button[data-code-tabs-button="1"]')
157+
158+
expect(activeButton).toBeTruthy()
159+
expect(inactiveButton).toBeTruthy()
160+
161+
expect(
162+
activeButton?.className.includes('hover:text-gray-600'),
163+
'Active tab should not change text color on hover'
164+
).toBe(false)
165+
166+
expect(
167+
inactiveButton?.className.includes('hover:text-gray-600'),
168+
'Inactive tab should change text color on hover'
169+
).toBe(true)
170+
})
171+
})
172+
173+
it('does not render tabs for excluded single-language blocks', async () => {
174+
await withJsdomEnvironment(async ({ window }) => {
175+
const { registerCodeTabsWebComponent } =
176+
await import('@components/Code/CodeTabs/client/index')
177+
registerCodeTabsWebComponent()
178+
179+
const element = window.document.createElement('code-tabs')
180+
const pre = window.document.createElement('pre')
181+
pre.setAttribute('data-language', 'text')
182+
pre.textContent = 'plain text'
183+
element.appendChild(pre)
184+
window.document.body.appendChild(element)
185+
186+
const anyTabButton = element.querySelector('button[data-code-tabs-button]')
187+
expect(anyTabButton, 'CodeTabs should not render tab buttons for excluded languages').toBeFalsy()
188+
})
189+
})
105190
})

‎src/components/Code/CodeTabs/client/index.ts‎

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,37 @@ function getCodeText(pre: HTMLPreElement): string {
3737
return text
3838
}
3939

40+
const EXCLUDED_SINGLE_TAB_LANGUAGES = new Set(['text', 'mermaid', 'math'])
41+
42+
function formatLanguageLabel(language: string): string {
43+
const normalized = language.trim().toLowerCase()
44+
const labelMap: Record<string, string> = {
45+
js: 'JavaScript',
46+
javascript: 'JavaScript',
47+
ts: 'TypeScript',
48+
typescript: 'TypeScript',
49+
html: 'HTML',
50+
css: 'CSS',
51+
json: 'JSON',
52+
yaml: 'YAML',
53+
yml: 'YAML',
54+
md: 'Markdown',
55+
}
56+
57+
return labelMap[normalized] ?? (language.trim() || language)
58+
}
59+
60+
function getTabLabel(pre: HTMLPreElement, index: number): string | null {
61+
const explicit = pre.getAttribute('data-code-tabs-tab')
62+
if (explicit && explicit.trim()) return explicit.trim()
63+
64+
const language = pre.getAttribute('data-language')
65+
if (!language || !language.trim()) return null
66+
if (EXCLUDED_SINGLE_TAB_LANGUAGES.has(language.trim().toLowerCase())) return null
67+
68+
return formatLanguageLabel(language)
69+
}
70+
4071
async function writeToClipboard(text: string): Promise<boolean> {
4172
try {
4273
const nav = (globalThis as unknown as { navigator?: Navigator }).navigator
@@ -57,6 +88,7 @@ class CodeTabsElement extends HTMLElement {
5788
private codeBlocks: HTMLPreElement[] = []
5889
private activeIndex = 0
5990
private copyTimer: number | null = null
91+
private isInteractiveTabs = false
6092

6193
connectedCallback(): void {
6294
if (this.dataset['enhanced'] === 'true') return
@@ -66,12 +98,16 @@ class CodeTabsElement extends HTMLElement {
6698

6799
if (this.codeBlocks.length === 0) return
68100

69-
const hasTabs = this.codeBlocks.length >= 2
70-
this.buildUi(hasTabs)
101+
const labels = this.codeBlocks
102+
.map((pre, index) => getTabLabel(pre, index))
103+
.filter((label): label is string => Boolean(label))
71104

72-
if (hasTabs) {
73-
this.setActive(0)
74-
}
105+
this.isInteractiveTabs = this.codeBlocks.length >= 2
106+
const shouldRenderTabs = this.isInteractiveTabs || (this.codeBlocks.length === 1 && labels.length === 1)
107+
108+
this.buildUi({ shouldRenderTabs, labels })
109+
110+
if (shouldRenderTabs) this.setActive(0)
75111
}
76112

77113
disconnectedCallback(): void {
@@ -81,26 +117,26 @@ class CodeTabsElement extends HTMLElement {
81117
}
82118
}
83119

84-
private buildUi(hasTabs: boolean): void {
120+
private buildUi(args: { shouldRenderTabs: boolean; labels: string[] }): void {
85121
const header = document.createElement('div')
86122
header.className =
87123
'flex w-full min-w-0 items-center justify-between border-b border-gray-200 bg-gray-50'
88124

89125
const copyButton = this.createCopyButton()
90126

91-
if (hasTabs) {
127+
if (args.shouldRenderTabs) {
92128
const list = document.createElement('ul')
93129
list.className = 'flex-1 min-w-0 p-0 whitespace-nowrap overflow-auto select-none'
94130

95-
this.tabButtons = this.codeBlocks.map((pre, index) => {
96-
const tabLabel = pre.getAttribute('data-code-tabs-tab') || `Tab ${index + 1}`
131+
this.tabButtons = this.codeBlocks.map((_, index) => {
132+
const tabLabel = args.labels[index] || `Tab ${index + 1}`
97133

98134
const li = document.createElement('li')
99135
li.className = 'list-none inline-block relative'
100136

101137
const button = document.createElement('button')
102138
button.type = 'button'
103-
button.className = 'inline-block px-2 py-1 m-2 text-gray-400 hover:text-gray-600'
139+
button.className = 'inline-block px-2 py-1 m-2 text-gray-400'
104140
button.textContent = tabLabel
105141
button.setAttribute('data-code-tabs-button', String(index))
106142

@@ -132,7 +168,7 @@ class CodeTabsElement extends HTMLElement {
132168
const button = document.createElement('button')
133169
button.type = 'button'
134170
button.className =
135-
'flex shrink-0 items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 border-l border-gray-200'
171+
'flex shrink-0 items-center gap-2 mr-2 px-3 py-2 text-gray-600 hover:text-gray-900 border-l border-gray-200'
136172
button.setAttribute('aria-label', tooltip)
137173
button.title = tooltip
138174

@@ -166,8 +202,19 @@ class CodeTabsElement extends HTMLElement {
166202
}
167203

168204
this.tabButtons.forEach((btn, i) => {
169-
btn.classList.toggle('text-gray-900', i === index)
170-
btn.classList.toggle('text-gray-400', i !== index)
205+
const isActive = i === index
206+
btn.classList.toggle('text-gray-900', isActive)
207+
btn.classList.toggle('text-gray-400', !isActive)
208+
209+
// Hover color behavior:
210+
// - Single-tab UI: no hover color change.
211+
// - Multi-tab UI: inactive tabs change color on hover; active tab does not.
212+
if (!this.isInteractiveTabs) {
213+
btn.classList.remove('hover:text-gray-600')
214+
return
215+
}
216+
217+
btn.classList.toggle('hover:text-gray-600', !isActive)
171218
})
172219
}
173220

‎src/components/Toc/index.astro‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const tocTree = buildTocTree(items)
5151
Jump to section
5252
</span>
5353
</div>
54+
<hr class="mt-4 border-dotted border-t-2 border-content-offset w-full" />
5455
<nav class="mt-4 toc-nav" aria-label="Table of contents">
5556
<ol class="space-y-1">
5657
{(() => {
@@ -61,7 +62,7 @@ const tocTree = buildTocTree(items)
6162
href={`#${node.slug}`}
6263
data-toc-link
6364
data-toc-slug={node.slug}
64-
class="block text-sm text-content-active transition-colors hover:text-primary data-[current=true]:text-content-active"
65+
class="block mb-2 text-sm sm:text-md text-content-offset transition-colors hover:text-content-active data-[current=true]:text-content no-underline hover:underline"
6566
>
6667
{node.text}
6768
</a>

‎src/content/articles/demo/index.mdx‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function fetchUserData(userId) {
7373
}, 500) // Simulate network delay
7474
})
7575
}
76+
7677
```
7778
```typescript [g1:Typescript]
7879
class TypedEventEmitter<T extends Record<keyof T, any>> implements EventEmitter<T> {
@@ -226,6 +227,39 @@ features:
226227
}
227228
}
228229
```
230+
```html [g3:HTML]
231+
<!DOCTYPE html>
232+
<html lang="en">
233+
<head>
234+
<meta charset="UTF-8">
235+
</head>
236+
<body style="margin: 0 auto; max-width: 520px;">
237+
<h1>Hi there!</h1>
238+
<div>I'm your cool new webpage! Use the toggle in the <dark-mode style="border: 2px solid blue"></dark-mode> 👈 button to switch my theme.</div>
239+
Github: <a href="https://github.com/jaywcjlove/dark-mode.git" target="__blank">@jaywcjlove/dark-mode</a>
240+
<script>
241+
const toggle = document.querySelector('dark-mode');
242+
const button = document.createElement('button');
243+
button.textContent = 'Change Theme';
244+
button.style = 'line-height: 16px';
245+
button.onclick = () => {
246+
const theme = document.documentElement.dataset.colorMode;
247+
toggle.mode = theme === 'light' ? 'dark' : 'light';
248+
// document.documentElement.setAttribute('data-color-mode', theme === 'light' ? 'dark' : 'light');
249+
}
250+
document.body.appendChild(button);
251+
document.addEventListener('colorschemechange', (e) => {
252+
console.log(`colorschemechange: Color scheme changed to "${e.detail.colorScheme}" or .`);
253+
button.textContent = toggle.mode === 'dark' ? 'Change Theme 🌞' : 'Change Theme 🌒';
254+
});
255+
document.addEventListener('permanentcolorscheme', (e) => {
256+
console.log(`~permanentcolorscheme: Color scheme changed to "${e.detail.colorScheme}" or .`);
257+
});
258+
const removePermanent = () => document.querySelectorAll('dark-mode').forEach((item) => item.removeAttribute('permanent'));
259+
</script>
260+
</body>
261+
</html>
262+
```
229263

230264
#### Add a title to the code block
231265

@@ -973,8 +1007,6 @@ my ==marked== text
9731007

9741008
### rehype-mathjax and remark-math
9751009

976-
- `rehype-katex` is alternative but lacks accessibility
977-
9781010
```markdown
9791011
Lift($$L$$) can be determined by Lift Coefficient ($$C_L$$) like the following
9801012
equation.

‎src/content/themes.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
{
2626
"id": "a11y",
2727
"name": "High Contrast",
28-
"description": "High contrast theme targeting WCAG 2.2 SC 1.4.6 (Contrast (Enhanced))",
28+
"description": "High contrast theme targeting WCAG 2.2 SC 1.4.6 Contrast (Enhanced)",
2929
"category": "core",
3030
"colors": {
3131
"backgroundOffset": "#e5e7eb"

‎src/lib/config/linkValidator.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export const linkValidatorPlugin = linkValidator({
2929
/** Skip hard-coded site URLs due to timeouts from CI to Vercel */
3030
'https://www.webstackbuilders.com',
3131
'https://www.webstackbuilders.com/rss.xml',
32+
/** Allow-listed external links to avoid timeouts */
33+
'https://webstackdev.github.io/',
34+
'https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies/',
35+
'https://typedoc.org',
3236
],
3337
externalTimeout: 15000,
3438
failOnBrokenLinks: true,

‎src/lib/config/markdown.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ export const markdownConfig: Partial<MdxOptions> = {
465465
themes: shikiConfigOptions.themes,
466466
defaultColor: shikiConfigOptions.defaultColor,
467467
langAlias: shikiConfigOptions.langAlias,
468-
wrap: false,
469-
excludeLangs: ['mermaid', 'math'],
468+
wrap: true,
469+
excludeLangs: ['text', 'mermaid', 'math'],
470470
},
471471
],
472472
/**

0 commit comments

Comments
 (0)