Skip to content

Commit 0d54185

Browse files
committed
feature: add search to website
- implement pagefind component - implement search filters
1 parent cd2d6b4 commit 0d54185

25 files changed

Lines changed: 725 additions & 109 deletions

File tree

.github/workflows/baseline.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ jobs:
145145
- name: "Generate Api References"
146146
run: "composer build:docs:api"
147147

148+
- name: "Setup Node.js"
149+
uses: actions/setup-node@v4
150+
with:
151+
node-version: '20'
152+
153+
- name: "Build Pagefind index"
154+
run: "npx --yes pagefind@1.5.2 --site web/landing/build --output-subdir pagefind"
155+
148156
- name: Pushes build to website repository
149157
uses: cpina/github-action-push-to-another-repository@main
150158
env:

web/landing/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ build/
44
vendor/
55
var/
66
public/assets
7+
public/pagefind
78
public/*.xml
89
!public/BingSiteAuth.xml
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
export default class extends Controller {
4+
static targets = ['mount'];
5+
static values = { bundlePath: String };
6+
7+
#ui = null;
8+
#onKey = null;
9+
#onModalToggle = null;
10+
11+
async connect() {
12+
this.#onKey = this.#handleKey.bind(this);
13+
this.#onModalToggle = this.#handleModalToggle.bind(this);
14+
15+
document.addEventListener('keydown', this.#onKey);
16+
this.element.addEventListener('toggle', this.#onModalToggle);
17+
}
18+
19+
disconnect() {
20+
if (this.#onKey) {
21+
document.removeEventListener('keydown', this.#onKey);
22+
}
23+
24+
if (this.#onModalToggle) {
25+
this.element.removeEventListener('toggle', this.#onModalToggle);
26+
}
27+
}
28+
29+
async #ensureUi() {
30+
if (this.#ui) return;
31+
if (!this.hasMountTarget) return;
32+
33+
await import(this.bundlePathValue + 'pagefind-ui.js');
34+
35+
// eslint-disable-next-line no-undef
36+
this.#ui = new PagefindUI({
37+
element: this.mountTarget,
38+
bundlePath: this.bundlePathValue,
39+
showSubResults: true,
40+
showImages: false,
41+
pageSize: 6,
42+
excerptLength: 24,
43+
resetStyles: false,
44+
translations: { placeholder: 'Search documentation…' },
45+
});
46+
}
47+
48+
#handleKey(event) {
49+
if (event.key !== '/') return;
50+
51+
const target = event.target;
52+
const tag = (target?.tagName || '').toLowerCase();
53+
54+
if (tag === 'input' || tag === 'textarea' || target?.isContentEditable) return;
55+
56+
event.preventDefault();
57+
this.element.showPopover();
58+
}
59+
60+
async #handleModalToggle(event) {
61+
if (event.newState !== 'open') return;
62+
63+
await this.#ensureUi();
64+
65+
setTimeout(() => {
66+
const input = this.element.querySelector('.pagefind-ui__search-input');
67+
input?.focus();
68+
}, 50);
69+
}
70+
}
Lines changed: 4 additions & 0 deletions
Loading

web/landing/assets/styles/app.css

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,3 +775,86 @@ code {
775775
@apply pointer-events-none opacity-50;
776776
}
777777
}
778+
779+
:root {
780+
--pagefind-ui-scale: 0.9;
781+
--pagefind-ui-primary: #ff5547;
782+
--pagefind-ui-text: rgba(255, 255, 255, 0.92);
783+
--pagefind-ui-background: transparent;
784+
--pagefind-ui-border: rgba(255, 255, 255, 0.10);
785+
--pagefind-ui-tag: rgba(255, 255, 255, 0.06);
786+
--pagefind-ui-border-width: 1px;
787+
--pagefind-ui-border-radius: 0.5rem;
788+
--pagefind-ui-image-border-radius: 0.5rem;
789+
--pagefind-ui-image-box-ratio: 3 / 2;
790+
--pagefind-ui-font: 'Cabin Variable', system-ui, sans-serif;
791+
}
792+
793+
#site-search .pagefind-ui__search-input {
794+
background: rgba(255, 255, 255, 0.04);
795+
color: rgba(255, 255, 255, 0.92);
796+
}
797+
798+
#site-search .pagefind-ui__search-input::placeholder {
799+
color: rgba(255, 255, 255, 0.45);
800+
}
801+
802+
#site-search .pagefind-ui__result {
803+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
804+
}
805+
806+
#site-search .pagefind-ui__result-link {
807+
color: #fff;
808+
}
809+
810+
#site-search .pagefind-ui__result-link:hover {
811+
color: #ff5547;
812+
}
813+
814+
#site-search .pagefind-ui__result-excerpt {
815+
color: rgba(255, 255, 255, 0.70);
816+
}
817+
818+
#site-search mark {
819+
background: transparent;
820+
color: #ff5547;
821+
font-weight: 600;
822+
}
823+
824+
#site-search .pagefind-ui__filter-panel,
825+
#site-search .pagefind-ui__filter-name,
826+
#site-search .pagefind-ui__filter-value {
827+
color: rgba(255, 255, 255, 0.85);
828+
}
829+
830+
#site-search .pagefind-ui__filter-name {
831+
font-weight: 600;
832+
text-transform: uppercase;
833+
letter-spacing: 0.08em;
834+
font-size: 0.75rem;
835+
color: rgba(255, 255, 255, 0.55);
836+
}
837+
838+
#site-search .pagefind-ui__filter-value input[type="checkbox"] {
839+
accent-color: #ff5547;
840+
}
841+
842+
#site-search .pagefind-ui__message {
843+
color: rgba(255, 255, 255, 0.65);
844+
}
845+
846+
#site-search .pagefind-ui__search-clear {
847+
background: transparent;
848+
border: 1px solid rgba(255, 255, 255, 0.10);
849+
color: rgba(255, 255, 255, 0.70);
850+
font-size: 0.75rem;
851+
padding: 0.35rem 0.6rem;
852+
border-radius: 0.375rem;
853+
transition: border-color 120ms, background 120ms, color 120ms;
854+
}
855+
856+
#site-search .pagefind-ui__search-clear:hover {
857+
background: rgba(255, 255, 255, 0.06);
858+
border-color: rgba(255, 255, 255, 0.20);
859+
color: #fff;
860+
}

web/landing/composer.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@
187187
"APP_ENV=prod bin/console presta:sitemaps:dump",
188188
"APP_ENV=prod bin/console static-content-generator:generate:routes --clean --parallel=8",
189189
"APP_ENV=prod bin/console static-content-generator:copy:assets -d public"
190+
],
191+
"build:search": [
192+
"@build",
193+
"npx --yes pagefind@1.5.2 --site build --output-subdir pagefind",
194+
"rm -rf public/pagefind",
195+
"cp -R build/pagefind public/pagefind"
190196
]
191197
}
192198
}

web/landing/config/services.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ services:
7070
arguments:
7171
$projectDir: '%kernel.project_dir%'
7272

73-
twig.markdown.league_common_mark_converter_factory:
74-
class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory
73+
Flow\Website\Service\Manifest\Manifest:
7574
arguments:
7675
$manifestPath: '%flow_root_dir%/manifest.json'
7776

77+
twig.markdown.league_common_mark_converter_factory:
78+
class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory
79+
7880
flow.telemetry.monolog.logger:
7981
class: Flow\Telemetry\Logger\Logger
8082
factory: ['@Flow\Telemetry\Telemetry', 'logger']

web/landing/phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
<server name="PANTHER_WEB_SERVER_ROUTER" value="../tests/router.php"/>
1616
</php>
1717
<testsuites>
18+
<testsuite name="unit">
19+
<directory>tests/Flow/Website/Tests/Unit</directory>
20+
</testsuite>
1821
<testsuite name="integration">
1922
<directory>tests/Flow/Website/Tests/Integration</directory>
2023
</testsuite>

web/landing/src/Flow/Website/Controller/DocumentationController.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
namespace Flow\Website\Controller;
66

7-
use Flow\Website\Model\Documentation\Module;
7+
use Flow\Website\Model\Documentation\{Module, Page};
88
use Flow\Website\Service\Documentation\{DSLDefinitions, Pages};
99
use Flow\Website\Service\Examples;
10+
use Flow\Website\Service\Manifest\PackageMeta;
1011
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1112
use Symfony\Component\HttpFoundation\Response;
1213
use Symfony\Component\Routing\Attribute\Route;
@@ -17,6 +18,7 @@ public function __construct(
1718
private readonly Pages $pages,
1819
private readonly DSLDefinitions $dslDefinitions,
1920
private readonly Examples $examples,
21+
private readonly PackageMeta $packageMeta,
2022
) {
2123
}
2224

@@ -78,6 +80,7 @@ public function dslFunction(string $module, string $function) : Response
7880
'definition' => $definition,
7981
'examples' => $examples,
8082
'types' => $this->dslDefinitions->types(),
83+
'searchFacets' => $this->dslFacets($module),
8184
]);
8285
}
8386

@@ -91,6 +94,7 @@ public function dslModule(string $module = 'core') : Response
9194
'modules' => $modules,
9295
'definitions' => $this->dslDefinitions->fromModule(Module::fromName($module)),
9396
'types' => $this->dslDefinitions->types(),
97+
'searchFacets' => $this->dslFacets($module),
9498
]);
9599
}
96100

@@ -111,14 +115,18 @@ public function example(string $topic, string $example) : Response
111115
'description' => $this->examples->description($currentTopic, $currentExample),
112116
'documentation' => $this->examples->documentation($currentTopic, $currentExample),
113117
'code' => $this->examples->code($currentTopic, $currentExample),
118+
'searchFacets' => ['type' => 'Example'],
114119
]);
115120
}
116121

117122
#[Route('/documentation', name: 'documentation', options: ['sitemap' => true])]
118123
public function index() : Response
119124
{
125+
$page = $this->pages->get('introduction.md');
126+
120127
return $this->render('documentation/page.html.twig', [
121-
'page' => $this->pages->get('introduction.md'),
128+
'page' => $page,
129+
'searchFacets' => $this->pageFacets($page),
122130
]);
123131
}
124132

@@ -144,8 +152,47 @@ public function navigationRight(string $currentPath = '') : Response
144152
#[Route('/documentation/{path}', name: 'documentation_page', requirements: ['path' => '.*'], priority: -100)]
145153
public function page(string $path) : Response
146154
{
155+
$page = $this->pages->get($path);
156+
147157
return $this->render('documentation/page.html.twig', [
148-
'page' => $this->pages->get($path),
158+
'page' => $page,
159+
'searchFacets' => $this->pageFacets($page),
149160
]);
150161
}
162+
163+
/**
164+
* @return array{type?: string, component?: string}
165+
*/
166+
private function dslFacets(string $module) : array
167+
{
168+
$facets = ['type' => 'DSL'];
169+
170+
$component = $this->packageMeta->forDslModule($module);
171+
172+
if ($component !== null) {
173+
$facets['component'] = $component;
174+
}
175+
176+
return $facets;
177+
}
178+
179+
/**
180+
* @return array{type?: string, component?: string}
181+
*/
182+
private function pageFacets(Page $page) : array
183+
{
184+
$packageName = $page->package();
185+
186+
if ($packageName === null) {
187+
return [];
188+
}
189+
190+
$meta = $this->packageMeta->forPackage($packageName);
191+
192+
if ($meta === null) {
193+
return [];
194+
}
195+
196+
return $meta;
197+
}
151198
}

web/landing/src/Flow/Website/Model/Documentation/Page.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ public function editOnGitHubUrl() : string
2828
return 'https://github.com/flow-php/flow/edit/1.x/documentation/' . ltrim($this->path, '/');
2929
}
3030

31+
public function package() : ?string
32+
{
33+
$frontMatterParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser());
34+
$result = $frontMatterParser->parse($this->content);
35+
$package = $result->getFrontMatter()['package'] ?? null;
36+
37+
return is_string($package) ? $package : null;
38+
}
39+
3140
public function title() : ?string
3241
{
3342
$frontMatterParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser());

0 commit comments

Comments
 (0)