Skip to content

Commit ced3f2a

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

File tree

10 files changed

+450
-66
lines changed

10 files changed

+450
-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: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
187+
const SectionHeading = headingLevel
188+
//const publicProps = componentProps?.filter((prop) => !prop.isHidden)
189+
const hasPropsToRender = !(typeof cssPrefix === 'undefined')
190+
191+
const onCollapse = (
192+
_event: React.MouseEvent,
193+
rowKey: number,
194+
isOpen: boolean,
195+
) => {
196+
const collapseAll = rowKey === undefined
197+
let newRows = Array.from(rows)
198+
199+
if (collapseAll) {
200+
newRows = newRows.map((r) =>
201+
r.isOpen === undefined ? r : { ...r, isOpen },
202+
)
203+
} else {
204+
newRows[rowKey] = { ...newRows[rowKey], isOpen }
205+
}
206+
setRows(newRows)
207+
}
208+
209+
const getDebouncedFilteredRows = debounce((value) => {
210+
const newSearchRE = new RegExp(value, 'i')
211+
setSearchRE(newSearchRE)
212+
setRows(getFilteredRows(newSearchRE))
213+
}, 500)
214+
215+
return (
216+
<div>
217+
<SectionHeading>CSS variables</SectionHeading>
218+
<Stack hasGutter>
219+
{hasPropsToRender && (
220+
<>
221+
<CSSSearch getDebouncedFilteredRows={getDebouncedFilteredRows} />
222+
<Table
223+
variant="compact"
224+
aria-label={`CSS Variables prefixed with ${cssPrefix}`}
225+
>
226+
<Thead>
227+
<Tr>
228+
{!hideSelectorColumn && (
229+
<React.Fragment>
230+
<Th screenReaderText="Expand or collapse column" />
231+
<Th>Selector</Th>
232+
</React.Fragment>
233+
)}
234+
<Th>Variable</Th>
235+
<Th>Value</Th>
236+
</Tr>
237+
</Thead>
238+
{!hideSelectorColumn ? (
239+
rows.map((row, rowIndex: number) => (
240+
<Tbody key={rowIndex} isExpanded={row.isOpen}>
241+
<Tr>
242+
<Td
243+
expand={
244+
row.details
245+
? {
246+
rowIndex,
247+
isExpanded: row.isOpen || false,
248+
onToggle: onCollapse,
249+
expandId: `css-vars-expandable-toggle-${cssPrefix}`,
250+
}
251+
: undefined
252+
}
253+
/>
254+
<Td dataLabel="Selector">{row.cells[0]}</Td>
255+
<Td dataLabel="Variable">{row.cells[1]}</Td>
256+
<Td dataLabel="Value">{row.cells[2]}</Td>
257+
</Tr>
258+
{row.details ? (
259+
<Tr isExpanded={row.isOpen}>
260+
{!row.details.fullWidth ? <Td /> : null}
261+
<Td dataLabel="Selector" colSpan={5}>
262+
{row.details.data}
263+
</Td>
264+
</Tr>
265+
) : null}
266+
</Tbody>
267+
))
268+
) : (
269+
<Tbody>
270+
{rows.map((row, rowIndex: number) => (
271+
<Tr key={rowIndex}>
272+
<Td dataLabel="Variable">{row.cells[0]}</Td>
273+
<Td dataLabel="Value">{row.cells[1]}</Td>
274+
</Tr>
275+
))}
276+
</Tbody>
277+
)}
278+
</Table>
279+
</>
280+
)}
281+
</Stack>
282+
</div>
283+
)
284+
}

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)