Skip to content

Commit dfaddc2

Browse files
committed
feat(CSSTable): Add CSS variable table
1 parent 90f7fc2 commit dfaddc2

File tree

10 files changed

+454
-66
lines changed

10 files changed

+454
-66
lines changed

src/components/CSSSearch.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState } from 'react'
2+
import { SearchInput } from '@patternfly/react-core'
3+
4+
interface CSSSearchProps {
5+
getDebouncedFilteredRows: (value: string) => void
6+
}
7+
8+
export const CSSSearch: React.FC<CSSSearchProps> = ({
9+
getDebouncedFilteredRows,
10+
}: CSSSearchProps) => {
11+
const [filterValue, setFilterValue] = useState('')
12+
13+
const onFilterChange = (
14+
_event: React.FormEvent<HTMLInputElement>,
15+
value: string,
16+
) => {
17+
setFilterValue(value)
18+
getDebouncedFilteredRows(value)
19+
}
20+
21+
return (
22+
<SearchInput
23+
aria-label="Filter CSS Variables"
24+
placeholder="Filter CSS Variables"
25+
value={filterValue}
26+
onChange={onFilterChange}
27+
/>
28+
)
29+
}

src/components/CSSTable.astro

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
import { CSSTable as CSSTableComponent } from './CSSTable'
3+
4+
const { cssPrefix } = Astro.props
5+
---
6+
7+
{
8+
cssPrefix.map((prefix: string, index: number) => (
9+
<CSSTableComponent key={index} cssPrefix={prefix} client:only="react" />
10+
))
11+
}

src/components/CSSTable.tsx

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { debounce, List, ListItem, Stack } from '@patternfly/react-core'
2+
import { Table, Thead, Th, Tr, Tbody, Td } from '@patternfly/react-table'
3+
import { useState } from 'react'
4+
import LevelUpAltIcon from '@patternfly/react-icons/dist/esm/icons/level-up-alt-icon'
5+
import * as tokensModule from '@patternfly/react-tokens/dist/esm/componentIndex'
6+
import React from 'react'
7+
import { CSSSearch } from './CSSSearch'
8+
9+
export type ComponentProp = {
10+
isOpen: boolean
11+
cells: [
12+
string,
13+
{
14+
type: string
15+
key: string
16+
ref: any
17+
props: any
18+
},
19+
]
20+
details: {
21+
parent: number
22+
fullWidth: boolean
23+
data: any
24+
}
25+
}
26+
27+
type Value = {
28+
name: string
29+
value: string
30+
values?: string[]
31+
}
32+
33+
type FileList = {
34+
[key: string]: {
35+
name: string
36+
value: string
37+
values?: Value[]
38+
}
39+
}
40+
41+
type List = {
42+
selector: string
43+
property: string
44+
token: string
45+
value: string
46+
values?: string[]
47+
}
48+
49+
type FilteredRows = {
50+
cells: (React.ReactNode | string | string[])[]
51+
isOpen?: boolean
52+
details?: { parent: number; fullWidth: boolean; data: React.ReactNode }
53+
}
54+
55+
interface CSSTableProps extends React.HTMLProps<HTMLDivElement> {
56+
cssPrefix: string
57+
headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
58+
hideSelectorColumn?: boolean
59+
selector?: string
60+
}
61+
62+
const isColorRegex = /^(#|rgb)/
63+
const mappingAsList = (property: string, values: string[]) => (
64+
<List isPlain>
65+
<ListItem>{property}</ListItem>
66+
{values.map((entry: string) => (
67+
<ListItem key={entry} icon={<LevelUpAltIcon className="rotate-90-deg" />}>
68+
{entry}
69+
</ListItem>
70+
))}
71+
</List>
72+
)
73+
74+
const flattenList = (files: FileList[]) => {
75+
const list = [] as List[]
76+
files.forEach((file) => {
77+
Object.entries(file).forEach(([selector, values]) => {
78+
if (values !== undefined) {
79+
Object.entries(values).forEach(([key, val]) => {
80+
if (typeof val === 'object' && val !== null && 'name' in val) {
81+
const v = val as unknown as Value
82+
list.push({
83+
selector,
84+
property: v.name,
85+
token: key,
86+
value: v.value,
87+
values: v.values,
88+
})
89+
}
90+
})
91+
}
92+
})
93+
})
94+
return list
95+
}
96+
97+
export const CSSTable: React.FunctionComponent<CSSTableProps> = ({
98+
cssPrefix,
99+
headingLevel = 'h3',
100+
hideSelectorColumn = false,
101+
selector,
102+
}) => {
103+
const prefixToken = cssPrefix.replace('pf-v6-', '').replace(/-+/g, '_')
104+
105+
const applicableFiles = Object.entries(tokensModule)
106+
.filter(([key, _val]) => prefixToken === key)
107+
.sort(([key1], [key2]) => key1.localeCompare(key2))
108+
.map(([_key, val]) => {
109+
const record = val as Record<string, any>
110+
111+
if (selector && record[selector]) {
112+
return record[selector]
113+
}
114+
115+
return Object.values(record)
116+
})
117+
.flat()
118+
119+
const flatList = flattenList(applicableFiles)
120+
121+
const getFilteredRows = (searchRE?: RegExp) => {
122+
const newFilteredRows = [] as FilteredRows[]
123+
let rowNumber = -1
124+
flatList.forEach((row) => {
125+
const { selector, property, value, values } = row
126+
const passes =
127+
!searchRE ||
128+
searchRE.test(selector) ||
129+
searchRE.test(property) ||
130+
searchRE.test(value) ||
131+
(values && searchRE.test(JSON.stringify(values)))
132+
if (passes) {
133+
const rowKey = `${selector}_${property}`
134+
const isColor = isColorRegex.test(value)
135+
const cells = [
136+
hideSelectorColumn ? [] : [selector],
137+
property,
138+
<div key={rowKey}>
139+
<div
140+
key={`${rowKey}_1`}
141+
className="pf-v6-l-flex pf-m-space-items-sm"
142+
>
143+
{isColor && (
144+
<div
145+
key={`${rowKey}_2`}
146+
className="pf-v6-l-flex pf-m-column pf-m-align-self-center"
147+
>
148+
<span
149+
className="circle"
150+
style={{ backgroundColor: `${value}` }}
151+
/>
152+
</div>
153+
)}
154+
<div
155+
key={`${rowKey}_3`}
156+
className="pf-v6-l-flex pf-m-column pf-m-align-self-center ws-td-text"
157+
>
158+
{isColor && '(In light theme)'} {value}
159+
</div>
160+
</div>
161+
</div>,
162+
]
163+
newFilteredRows.push({
164+
isOpen: values ? false : undefined,
165+
cells,
166+
details: values
167+
? {
168+
parent: rowNumber,
169+
fullWidth: true,
170+
data: mappingAsList(property, values),
171+
}
172+
: undefined,
173+
})
174+
rowNumber += 1
175+
if (values) {
176+
rowNumber += 1
177+
}
178+
}
179+
})
180+
return newFilteredRows
181+
}
182+
183+
const INITIAL_REGEX = /.*/
184+
const [searchRE, setSearchRE] = useState<RegExp>(INITIAL_REGEX)
185+
const [rows, setRows] = useState(getFilteredRows(searchRE))
186+
const [allRowsExpanded, setAllRowsExpanded] = useState(true)
187+
188+
const SectionHeading = headingLevel
189+
//const publicProps = componentProps?.filter((prop) => !prop.isHidden)
190+
const hasPropsToRender = !(typeof cssPrefix === 'undefined')
191+
192+
const onCollapse = (
193+
_event: React.MouseEvent,
194+
rowKey: number,
195+
isOpen: boolean,
196+
) => {
197+
const collapseAll = rowKey === undefined
198+
let newRows = Array.from(rows)
199+
200+
if (collapseAll) {
201+
newRows = newRows.map((r) =>
202+
r.isOpen === undefined ? r : { ...r, isOpen },
203+
)
204+
} else {
205+
newRows[rowKey] = { ...newRows[rowKey], isOpen }
206+
}
207+
setRows(newRows)
208+
if (collapseAll) {
209+
setAllRowsExpanded((prevState) => !prevState)
210+
}
211+
}
212+
213+
const getDebouncedFilteredRows = debounce((value) => {
214+
const newSearchRE = new RegExp(value, 'i')
215+
setSearchRE(newSearchRE)
216+
setRows(getFilteredRows(newSearchRE))
217+
}, 500)
218+
219+
return (
220+
<div>
221+
<SectionHeading>CSS variables</SectionHeading>
222+
<Stack hasGutter>
223+
{hasPropsToRender && (
224+
<>
225+
<CSSSearch getDebouncedFilteredRows={getDebouncedFilteredRows} />
226+
<Table
227+
variant="compact"
228+
aria-label={`CSS Variables prefixed with ${cssPrefix}`}
229+
>
230+
<Thead>
231+
<Tr>
232+
{!hideSelectorColumn && (
233+
<React.Fragment>
234+
<Th screenReaderText="Expand or collapse column" />
235+
<Th>Selector</Th>
236+
</React.Fragment>
237+
)}
238+
<Th>Variable</Th>
239+
<Th>Value</Th>
240+
</Tr>
241+
</Thead>
242+
{!hideSelectorColumn ? (
243+
rows.map((row, rowIndex: number) => (
244+
<Tbody key={rowIndex} isExpanded={row.isOpen}>
245+
<Tr>
246+
<Td
247+
expand={
248+
row.details
249+
? {
250+
rowIndex,
251+
isExpanded: row.isOpen || false,
252+
onToggle: onCollapse,
253+
expandId: `css-vars-expandable-toggle-${cssPrefix}`,
254+
}
255+
: undefined
256+
}
257+
/>
258+
<Td dataLabel="Selector">{row.cells[0]}</Td>
259+
<Td dataLabel="Variable">{row.cells[1]}</Td>
260+
<Td dataLabel="Value">{row.cells[2]}</Td>
261+
</Tr>
262+
{row.details ? (
263+
<Tr isExpanded={row.isOpen}>
264+
{!row.details.fullWidth ? <Td /> : null}
265+
<Td dataLabel="Selector" colSpan={5}>
266+
{row.details.data}
267+
</Td>
268+
</Tr>
269+
) : null}
270+
</Tbody>
271+
))
272+
) : (
273+
<Tbody>
274+
{rows.map((row, rowIndex: number) => (
275+
<Tr key={rowIndex}>
276+
<Td dataLabel="Variable">{row.cells[0]}</Td>
277+
<Td dataLabel="Value">{row.cells[1]}</Td>
278+
</Tr>
279+
))}
280+
</Tbody>
281+
)}
282+
</Table>
283+
</>
284+
)}
285+
</Stack>
286+
</div>
287+
)
288+
}

src/content.config.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ function defineContent(contentObj: CollectionDefinition) {
1717
// TODO: Expand for other packages that remain under the react umbrella (Table, CodeEditor, etc)
1818
const tabMap: any = {
1919
'react-component-docs': 'react',
20-
'core-component-docs': 'html'
21-
};
20+
'core-component-docs': 'html',
21+
}
2222

2323
return defineCollection({
2424
loader: glob({ base: dir, pattern }),
@@ -28,7 +28,18 @@ function defineContent(contentObj: CollectionDefinition) {
2828
subsection: z.string().optional(),
2929
title: z.string().optional(),
3030
propComponents: z.array(z.string()).optional(),
31-
tab: z.string().optional().default(tabMap[name])
31+
tab: z.string().optional().default(tabMap[name]),
32+
cssPrefix: z
33+
.preprocess((val) => {
34+
if (typeof val === 'string') {
35+
return [val]
36+
}
37+
if (Array.isArray(val)) {
38+
return val
39+
}
40+
return undefined
41+
}, z.array(z.string()))
42+
.optional(),
3243
}),
3344
})
3445
}

src/layouts/Main.astro

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
import '@patternfly/patternfly/patternfly.css'
3+
import '../styles/global.scss'
34
import { ClientRouter } from 'astro:transitions'
45
56
import Page from '../components/Page.astro'
67
import Masthead from '../components/Masthead.astro'
78
import Navigation from '../components/Navigation.astro'
8-
99
---
1010

1111
<html lang="en" transition:animate="none">
@@ -27,11 +27,15 @@ import Navigation from '../components/Navigation.astro'
2727
</html>
2828

2929
<script>
30-
const themePreference = window.localStorage.getItem ('theme-preference');
31-
document?.querySelector('html')?.classList.toggle('pf-v6-theme-dark', themePreference === 'dark' );
32-
33-
document.addEventListener("astro:after-swap", () => {
34-
const themePreference = window.localStorage.getItem ('theme-preference');
35-
document?.querySelector('html')?.classList.toggle('pf-v6-theme-dark', themePreference === 'dark' );
36-
});
30+
const themePreference = window.localStorage.getItem('theme-preference')
31+
document
32+
?.querySelector('html')
33+
?.classList.toggle('pf-v6-theme-dark', themePreference === 'dark')
34+
35+
document.addEventListener('astro:after-swap', () => {
36+
const themePreference = window.localStorage.getItem('theme-preference')
37+
document
38+
?.querySelector('html')
39+
?.classList.toggle('pf-v6-theme-dark', themePreference === 'dark')
40+
})
3741
</script>

0 commit comments

Comments
 (0)