Skip to content

Commit 1e69ed1

Browse files
committed
feat(CSSTable): Add CSS variable table
1 parent bbf7493 commit 1e69ed1

File tree

10 files changed

+397
-32
lines changed

10 files changed

+397
-32
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: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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+
if (selector) {
110+
return {
111+
selector: (val as Record<string, any>)[selector],
112+
}
113+
}
114+
return val
115+
})
116+
117+
const flatList = flattenList(applicableFiles as any)
118+
119+
const getFilteredRows = (searchRE?: RegExp) => {
120+
const newFilteredRows = [] as FilteredRows[]
121+
let rowNumber = -1
122+
flatList.forEach((row) => {
123+
const { selector, property, value, values } = row
124+
const passes =
125+
!searchRE ||
126+
searchRE.test(selector) ||
127+
searchRE.test(property) ||
128+
searchRE.test(value) ||
129+
(values && searchRE.test(JSON.stringify(values)))
130+
if (passes) {
131+
const rowKey = `${selector}_${property}`
132+
const isColor = isColorRegex.test(value)
133+
const cells = [
134+
hideSelectorColumn ? [] : [selector],
135+
property,
136+
<div key={rowKey}>
137+
<div
138+
key={`${rowKey}_1`}
139+
className="pf-v6-l-flex pf-m-space-items-sm"
140+
>
141+
{isColor && (
142+
<div
143+
key={`${rowKey}_2`}
144+
className="pf-v6-l-flex pf-m-column pf-m-align-self-center"
145+
>
146+
<span
147+
className="circle"
148+
style={{ backgroundColor: `${value}` }}
149+
/>
150+
</div>
151+
)}
152+
<div
153+
key={`${rowKey}_3`}
154+
className="pf-v6-l-flex pf-m-column pf-m-align-self-center ws-td-text"
155+
>
156+
{isColor && '(In light theme)'} {value}
157+
</div>
158+
</div>
159+
</div>,
160+
]
161+
newFilteredRows.push({
162+
isOpen: values ? false : undefined,
163+
cells,
164+
details: values
165+
? {
166+
parent: rowNumber,
167+
fullWidth: true,
168+
data: mappingAsList(property, values),
169+
}
170+
: undefined,
171+
})
172+
rowNumber += 1
173+
if (values) {
174+
rowNumber += 1
175+
}
176+
}
177+
})
178+
return newFilteredRows
179+
}
180+
181+
const INITIAL_REGEX = /.*/
182+
const [searchRE, setSearchRE] = useState<RegExp>(INITIAL_REGEX)
183+
const [rows, setRows] = useState(getFilteredRows(searchRE))
184+
185+
const SectionHeading = headingLevel
186+
//const publicProps = componentProps?.filter((prop) => !prop.isHidden)
187+
const hasPropsToRender = !(typeof cssPrefix === 'undefined')
188+
189+
const onCollapse = (
190+
_event: React.MouseEvent,
191+
rowKey: number,
192+
isOpen: boolean,
193+
) => {
194+
const collapseAll = rowKey === undefined
195+
let newRows = Array.from(rows)
196+
197+
if (collapseAll) {
198+
newRows = newRows.map((r) =>
199+
r.isOpen === undefined ? r : { ...r, isOpen },
200+
)
201+
} else {
202+
newRows[rowKey] = { ...newRows[rowKey], isOpen }
203+
}
204+
setRows(newRows)
205+
}
206+
207+
const getDebouncedFilteredRows = debounce((value) => {
208+
const newSearchRE = new RegExp(value, 'i')
209+
setSearchRE(newSearchRE)
210+
setRows(getFilteredRows(newSearchRE))
211+
}, 500)
212+
213+
return (
214+
<div>
215+
<SectionHeading>CSS variables</SectionHeading>
216+
<Stack hasGutter>
217+
{hasPropsToRender && (
218+
<>
219+
<CSSSearch getDebouncedFilteredRows={getDebouncedFilteredRows} />
220+
<Table
221+
variant="compact"
222+
aria-label={`CSS Variables prefixed with ${cssPrefix}`}
223+
>
224+
<Thead>
225+
<Tr>
226+
{!hideSelectorColumn && (
227+
<React.Fragment>
228+
<Th screenReaderText="Expand or collapse column" />
229+
<Th>Selector</Th>
230+
</React.Fragment>
231+
)}
232+
<Th>Variable</Th>
233+
<Th>Value</Th>
234+
</Tr>
235+
</Thead>
236+
{!hideSelectorColumn ? (
237+
rows.map((row, rowIndex: number) => (
238+
<Tbody key={rowIndex} isExpanded={row.isOpen}>
239+
<Tr>
240+
<Td
241+
expand={
242+
row.details
243+
? {
244+
rowIndex,
245+
isExpanded: row.isOpen || false,
246+
onToggle: onCollapse,
247+
expandId: `css-vars-expandable-toggle-${cssPrefix}`,
248+
}
249+
: undefined
250+
}
251+
/>
252+
<Td dataLabel="Selector">{row.cells[0]}</Td>
253+
<Td dataLabel="Variable">{row.cells[1]}</Td>
254+
<Td dataLabel="Value">{row.cells[2]}</Td>
255+
</Tr>
256+
{row.details ? (
257+
<Tr isExpanded={row.isOpen}>
258+
{!row.details.fullWidth ? <Td /> : null}
259+
<Td dataLabel="Selector" colSpan={5}>
260+
{row.details.data}
261+
</Td>
262+
</Tr>
263+
) : null}
264+
</Tbody>
265+
))
266+
) : (
267+
<Tbody>
268+
{rows.map((row, rowIndex: number) => (
269+
<Tr key={rowIndex}>
270+
<Td dataLabel="Variable">{row.cells[0]}</Td>
271+
<Td dataLabel="Value">{row.cells[1]}</Td>
272+
</Tr>
273+
))}
274+
</Tbody>
275+
)}
276+
</Table>
277+
</>
278+
)}
279+
</Stack>
280+
</div>
281+
)
282+
}

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)