Skip to content
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
eea30cd
feat!: drop nodejs v18 support
azat-io Aug 10, 2025
7f24360
feat!: move to esm only
azat-io Aug 10, 2025
3f840fc
chore: update dependencies
azat-io Aug 10, 2025
50cbf85
chore: use esm-compatible dirname in vite config
azat-io Aug 10, 2025
0d7c6c2
feat(sort-decorators): add array-based custom groups api
hugop95 Sep 10, 2025
7298b83
feat!: drop group kind support
hugop95 Sep 13, 2025
4988fce
feat(sort-heritage-clauses): add array-based custom groups api
hugop95 Sep 13, 2025
c174d95
chore: update dependencies
azat-io Sep 13, 2025
377292d
feat(sort-imports)!: drop deprecated ts config root dir support
hugop95 Sep 13, 2025
a46deab
feat!: drop deprecated selectors support for multiple rules
hugop95 Sep 13, 2025
8834f41
feat!: drop deprecated object-based custom groups support
hugop95 Sep 14, 2025
20b6577
feat: support annotation-based config
hugop95 Sep 14, 2025
61cabe0
feat(sort-object-types)!: drop deprecated ignore pattern option
hugop95 Sep 19, 2025
9521e9f
feat(sort-jsx-props)!: drop deprecated ignore pattern option
hugop95 Sep 19, 2025
a2ffa76
feat: add sort-import-attributes rule
azat-io Sep 19, 2025
cab4d0f
feat: add sort-export-attributes rule
azat-io Sep 19, 2025
1779615
chore: update dependencies
azat-io Sep 19, 2025
f312706
feat(sort-object)!: drop deprecated destructure only option
hugop95 Sep 22, 2025
ee31e2b
fix: fix plugin usage with legacy configurations
azat-io Sep 25, 2025
ae9817b
chore: update dependencies
azat-io Sep 25, 2025
cd117fa
feat(sort-objects): add pattern matching for variable declarations
hugop95 Sep 27, 2025
0c01411
chore: update dependencies
azat-io Oct 2, 2025
a01b8f3
fix: keep settings priority when meta default options provided
azat-io Oct 2, 2025
576712c
chore: update eslint plugin
azat-io Oct 2, 2025
b0c8408
refactor: move typescript eslint types to dev dependencies
azat-io Oct 4, 2025
a5626e8
feat!: drop deprecated newlines between always and never
hugop95 Oct 7, 2025
65cd8ed
feat(sort-objects)!: migrate object type options to conditional confi…
hugop95 Oct 14, 2025
a65b39e
feat(sort-enums)!: replace force numeric sort and update default sort…
hugop95 Oct 14, 2025
0bc49f4
feat(sort-objects): add numeric keys detection option
hugop95 Oct 30, 2025
102a018
feat(sort-object-types): add numeric keys detection option
hugop95 Oct 31, 2025
e7d8f33
docs: fix memory leaks on website
azat-io Oct 31, 2025
f56267f
docs: add search
azat-io Nov 1, 2025
03eb5de
docs: fix astro warnings
azat-io Nov 1, 2025
5032f1d
chore: update dependencies
azat-io Nov 2, 2025
590ce8e
feat(sort-imports): add multiline and singleline modifiers
hugop95 Nov 9, 2025
8f2126c
feat!: improve comment above integration in groups
hugop95 Nov 16, 2025
d0b38b9
fix: require sorting type in fallback sort schema
hugop95 Nov 16, 2025
513d13e
feat: allow type overrides in groups option
hugop95 Nov 17, 2025
98404bf
feat(sort-imports)!: drop deprecated selectors
hugop95 Nov 18, 2025
f9919cb
feat: allow order overrides in groups option
hugop95 Nov 18, 2025
885b3a8
feat: allow new lines inside overrides in groups option
hugop95 Nov 21, 2025
4977783
chore: update github actions
azat-io Nov 22, 2025
4e03287
chore: update dependencies
azat-io Nov 22, 2025
0403705
docs: enable svg optimization
azat-io Nov 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
"changelogen",
"changelogithub",
"crosspost",
"frontmatter",
"grotesk",
"humanwhocodes",
"joshuakgoldberg",
"keyux",
"libc",
"lightningcss",
"mdast",
"mdxjs",
"minisearch",
"nanostores",
"nocomment",
"pcss",
Expand Down
14 changes: 4 additions & 10 deletions astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,12 @@ export default defineConfig({
],
},
integrations: [
svelte({
compilerOptions: {
cssHash: ({ hash, css }) => `s-${hash(css)}`,
discloseVersion: false,
},
}),
svelte(),
sitemap({
filter: page => !new RegExp(`^${site}/guide$`, 'u').test(page),
}),
mdx(),
],
prefetch: {
defaultStrategy: 'viewport',
prefetchAll: true,
},
build: {
inlineStylesheets: 'always',
format: 'file',
Expand All @@ -79,6 +70,9 @@ export default defineConfig({
clientPrerender: true,
},
publicDir: path.join(dirname, './docs/public'),
prefetch: {
defaultStrategy: 'hover',
},
server: {
port: 3000,
host: true,
Expand Down
5 changes: 4 additions & 1 deletion docs/components/CodeTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@

onMount(() => {
mounted = true
startKeyUX(globalThis, [focusGroupKeyUX()])
let stop = startKeyUX(globalThis, [focusGroupKeyUX()])
return () => {
stop()
}
})
</script>

Expand Down
9 changes: 8 additions & 1 deletion docs/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,14 @@ let isProduction = import.meta.env.PROD
localStorage.setItem('theme', theme)
}
initialize()
document.addEventListener('astro:after-swap', initialize)
let handlerKey = '__perfectionistThemeAfterSwap__'
if (!globalThis[handlerKey]) {
let handleAfterSwap = () => {
initialize()
}
document.addEventListener('astro:after-swap', handleAfterSwap)
globalThis[handlerKey] = handleAfterSwap
}
</script>

<ClientRouter />
Expand Down
90 changes: 67 additions & 23 deletions docs/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import SkipToContent from './SkipToContent.astro'
import Navigation from './Navigation.astro'
import IconMenu from '../icons/menu.svg'
import Search from './Search.svelte'
import Logo from './Logo.astro'

interface Props {
Expand All @@ -27,49 +28,92 @@ let { border = false } = Astro.props
<span class="title">Perfectionist</span>
</a>
</div>
<Search client:load />
<Navigation />
</header>

<script>
import { toggleMenu } from '../stores/menu-open'

let initMenu = () => {
document
.getElementById('menu-button')!
.addEventListener('click', toggleMenu)
let menuController: AbortController | undefined
let scrollController: AbortController | undefined

let cleanup = () => {
menuController?.abort()
scrollController?.abort()
menuController = undefined
scrollController = undefined
}

document.addEventListener('astro:after-swap', initMenu)
initMenu()
</script>
let setupMenu = () => {
let menuButton = document.getElementById('menu-button')
if (!menuButton) {
return
}

<script>
let initHeader = () => {
let header = document.querySelector('.header')!

document.addEventListener('scroll', () => {
let { scrollY } = globalThis

if (!header.classList.contains('border')) {
if (scrollY > 24) {
header.classList.add('scroll')
} else {
header.classList.remove('scroll')
}
menuController = new AbortController()
menuButton.addEventListener('click', toggleMenu, {
signal: menuController.signal,
})
}

let setupScroll = () => {
let header = document.querySelector('.header')
if (!header) {
return
}

let handleScroll = () => {
if (header.classList.contains('border')) {
return
}

header.classList.toggle('scroll', globalThis.scrollY > 24)
}

scrollController = new AbortController()
document.addEventListener('scroll', handleScroll, {
signal: scrollController.signal,
passive: true,
})
handleScroll()
}

let scheduleNext = () => {
document.addEventListener(
'astro:before-swap',
() => {
cleanup()
},
{ once: true },
)
document.addEventListener(
'astro:after-swap',
() => {
setup()
},
{ once: true },
)
}

let setup = () => {
cleanup()
setupMenu()
setupScroll()
scheduleNext()
}

document.addEventListener('astro:after-swap', initHeader)
initHeader()
setup()
</script>

<style>
.header {
position: sticky;
inset-block-start: 0;
z-index: 2;
display: flex;
display: grid;
grid-template-columns: auto auto 1fr;
gap: var(--space-m);
align-items: center;
justify-content: space-between;
block-size: var(--header-block-size);
Expand Down
81 changes: 81 additions & 0 deletions docs/components/HighlightText.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script lang="ts">
let { terms, text }: { terms: string[]; text: string } = $props()

interface Chunk {
highlight: boolean
value: string
}

let chunks = $derived(createChunks(text, terms))

function createChunks(value: string, highlightTerms: string[]): Chunk[] {
if (!value) {
return []
}

let uniqueTerms = [
...new Map(
highlightTerms.filter(Boolean).map(term => [term.toLowerCase(), term]),
).values(),
]

if (uniqueTerms.length === 0) {
return [{ highlight: false, value }]
}

uniqueTerms.sort((a, b) => b.length - a.length)

let escaped = uniqueTerms
.map(term => term.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`))
.join('|')

let pattern = new RegExp(`(${escaped})`, 'giu')
let result: Chunk[] = []
let lastIndex = 0

value.replace(
pattern,
(match: string, _group: string, offset: number): string => {
if (offset > lastIndex) {
result.push({
value: value.slice(lastIndex, offset),
highlight: false,
})
}

result.push({
value: value.slice(offset, offset + match.length),
highlight: true,
})

lastIndex = offset + match.length
return match
},
)

if (lastIndex < value.length) {
result.push({
value: value.slice(lastIndex),
highlight: false,
})
}

return result
}
</script>

{#each chunks as chunk, index (index)}
{#if chunk.highlight}
<mark data-index={index}>{chunk.value}</mark>
{:else}
{chunk.value}
{/if}
{/each}

<style>
mark {
padding-inline: 1px;
color: inherit;
background: var(--color-overlay-brand);
}
</style>
54 changes: 47 additions & 7 deletions docs/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,55 @@ import IconSun from '../icons/sun.svg'
<script>
import { toggleTheme } from '../stores/theme'

let initToggleTheme = () => {
document
.getElementById('toggle-theme')!
.addEventListener('click', toggleTheme)
let controller: AbortController | undefined

let cleanup = () => {
controller?.abort()
controller = undefined
}

let scheduleNext = () => {
document.addEventListener(
'astro:before-swap',
() => {
cleanup()
},
{ once: true },
)
document.addEventListener(
'astro:after-swap',
() => {
setup()
},
{ once: true },
)
}

document.addEventListener('astro:after-swap', initToggleTheme)
initToggleTheme()
let setup = () => {
cleanup()

let toggleButton = document.getElementById('toggle-theme')
if (!toggleButton) {
scheduleNext()
return
}

controller = new AbortController()
toggleButton.addEventListener('click', toggleTheme, {
signal: controller.signal,
})

scheduleNext()
}

setup()
</script>

<style>
.navigation {
display: flex;
align-items: start;
justify-content: end;
}

.list {
Expand Down Expand Up @@ -105,6 +141,10 @@ import IconSun from '../icons/sun.svg'
block-size: var(--size-icon-m);
}

.item {
margin-block: 0;
}

.hr,
.item-text {
display: none;
Expand Down Expand Up @@ -133,7 +173,7 @@ import IconSun from '../icons/sun.svg'

.hr,
.item-text {
display: block;
display: inline;
}
}
</style>
17 changes: 17 additions & 0 deletions docs/components/Portal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
function portal(node: HTMLElement): {
destroy(): void
} {
document.body.append(node)

return {
destroy() {
node.remove()
},
}
}
Comment on lines +2 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add SSR safety guard for document.body access.

Directly accessing document.body will throw a ReferenceError during server-side rendering if this documentation site uses SSR (e.g., SvelteKit, Astro).

Apply this diff to guard the portal initialization:

  function portal(node: HTMLElement): {
    destroy(): void
  } {
-   document.body.append(node)
+   if (typeof document !== 'undefined') {
+     document.body.append(node)
+   }

    return {
      destroy() {
-       node.remove()
+       if (typeof document !== 'undefined') {
+         node.remove()
+       }
      },
    }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function portal(node: HTMLElement): {
destroy(): void
} {
document.body.append(node)
return {
destroy() {
node.remove()
},
}
}
function portal(node: HTMLElement): {
destroy(): void
} {
if (typeof document !== 'undefined') {
document.body.append(node)
}
return {
destroy() {
if (typeof document !== 'undefined') {
node.remove()
}
},
}
}
🤖 Prompt for AI Agents
In docs/components/Portal.svelte around lines 2 to 12, the portal() action
directly uses document.body which breaks during SSR; guard the initialization by
checking that typeof document !== "undefined" and that document.body exists
before calling document.body.append(node), and if not available return an object
whose destroy is a no-op; also make the destroy safe in the browser by removing
the node only if it is attached (e.g., node.parentNode or node.isConnected) to
avoid runtime errors.

</script>

<div use:portal>
<slot />
</div>
Loading