diff --git a/.gitignore b/.gitignore index 143e55e1..e501bc92 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Production /website/build +/website/build-marketing # Generated files /website/.docusaurus diff --git a/AGENTS.md b/AGENTS.md index 1b041985..f5e9280a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,16 @@ The canonical task→convention dispatch table lives in `~/.claude/agentics/temp ## Project context Overture Docs is the **centralized documentation site for the Overture stack**, built with [Docusaurus](https://docusaurus.io/). It aggregates Markdown from the `/docs` directory of each Overture project repository (pulled in as git submodules) and renders them as one navigable site. The point of the setup is that documentation stays with its source project while readers get a single hub. Full overview in [README.md](README.md); planned work lives in `.dev/roadmap.md`. +## Verifying a UI change + +A clean `npm run build` says a page compiled, not that it looks right. Before calling any visual change done, render it and look at it. + +- Build and serve: `cd website && npm run build && npm run serve --port `. `npm start` renders in the browser, so its HTML is an empty shell and tells you nothing. +- Screenshot with a real browser at a real viewport. Chrome's `--headless --screenshot --window-size` is unreliable: the window size is not the CSS viewport, and it will show you clipping and overflow that do not exist. Drive it with `puppeteer-core` and `page.setViewport` instead, pointing `executablePath` at the installed Chrome. Install it outside this repo (the scratchpad), never as a dependency here. +- Check the widths that change behaviour, not just the one you are working at. The marketing navbar collapses at 1160px. +- When something looks wrong, measure it in the page (`getComputedStyle`, `getBoundingClientRect`, `scrollWidth` against `clientWidth`) before changing CSS. Reading a screenshot is guessing. +- Check a documentation page too whenever marketing styles or theme components change: the two sites are separate builds but one codebase, and `src/theme/` and the emitted stylesheet are shared by both. `npm run build` and `npm run build:marketing` both have to pass. + ## Project-specific constraints - **Public repository** (`overture-stack/docs`): no credentials, secrets, tokens, or private URLs in any committed file, ever. - **Documentation content is owned by the submodules, not this repo.** Each project's docs live in `submodules//docs/` and are symlinked into `website/docs/`. Edit the **source** file under `submodules//`, never the symlinked copy under `website/docs/`: editing through the link mutates the vendored submodule's working tree in a way that is easy to lose and confusing to review. Symlinks are (re)generated by `symlinker.sh`. @@ -24,7 +34,11 @@ Overture Docs is the **centralized documentation site for the Overture stack**, ## Repository orientation - `submodules/` — vendored Overture repos as git submodules: `arranger`, `lectern`, `lyric`, `maestro`, `score`, `song`, `stage`, and `.github`. Each carries its own `/docs`. Defined in `.gitmodules` (see `.dev/roadmap.md`: release-branch tracking is not yet configured, so `git submodule update --remote` currently follows each repo's default branch). -- `website/` — the Docusaurus site: `docs/` (aggregated, symlinked from submodules), `community/`, `guides/`, and `src/` (`components/`, `css/`, `theme/`, `pages/`). +- `website/` — the Docusaurus site: `docs/` (aggregated, symlinked from submodules), `community/`, `guides/`, and `src/` (`components/`, `css/`, `marketing/`, `theme/`, `pages/`). +- `website/src/marketing/`: everything the overture.bio site is made of: its routes under `pages/` (whose `index.tsx` is the home page at `/`), plus components, constants, case-study data, and the Sass tree. Its styles are all imported inside `.marketing` by `styles/index.scss`, and only `MarketingPage.tsx` imports that file, which is what kept those bare class names and element rules off the documentation pages while one build served both. The builds are separate now, so this is belt and braces, but the scoping stays: `src/theme/` is still shared. Add marketing styles as a partial imported there, never as a stray `import "./styles.scss"` in a component. - `symlinker.sh` — regenerates the symlinks that bring selected submodule `/docs` files into `website/docs/`. +- **Two sites, two builds, one branch.** `docusaurus.config.ts` reads `OVERTURE_SITE`: unset it builds docs.overture.bio (`npm run build`), `marketing` builds overture.bio (`npm run build:marketing`). The marketing build drops the documentation plugin instances and the redirect table, and points the pages plugin at `src/marketing/pages/`. Both builds keep Algolia: `MarketingNavbar` renders the same `@theme/SearchBar`, and the marketing build adds `externalUrlRegex` so results navigate cross-host to docs.overture.bio instead of 404ing. Components ask which build they are in through `useIsMarketingSite`, never by inspecting the route. +- `website/static-docs/` and `website/static-marketing/` — what each host owns alone: `robots.txt` for both, and the `_redirects` file each one publishes. `website/static/` is still shared. There is deliberately no `netlify.toml`: one file on one branch cannot say different things to two Netlify sites, and `docs.overture.bio` is configured through the Netlify UI. Redirects belong in the `_redirects` file of the host that owes them. +- `website-legacy/` is **gone** as of 2026-08-07. It was a staged copy of the [overture-stack/website](https://github.com/overture-stack/website) Gatsby site, kept while the port ran. Everything it held is either ported into `website/` or still in that repo, which serves overture.bio until DNS is cut over to the marketing build here, and is cloned locally at `~/Desktop/website`. Until that cutover, a change that has to reach the live overture.bio goes there, not here. - `README.md`, `preview.png` — human-facing project overview and screenshot. - Local setup: Node 18+, `npm ci`, then `npm start` (see `README.md` § Getting Started). diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 46a080b2..d0865257 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,35 @@ cd website npm ci ``` -3. Start the server: +3. Start the server for whichever of the two sites you are working on: ```bash -npm start +npm start # docs.overture.bio, the documentation site +npm run start:marketing # overture.bio, the marketing site ``` +Both serve on port 3000, so to run them side by side give one of them another port: `npm run start:marketing -- --port 3001`. + > [!IMPORTANT] > Docusaurus requires node version 18 or higher. All npm commands run from `website/`; there is no package manifest at the repository root. +> [!TIP] +> One repository, two sites. `npm start` serves the documentation with its homepage at `/`; `npm run start:marketing` serves the marketing site with **its** homepage at `/`. Neither contains the other's routes. [Two builds, one branch](#the-overturebio-marketing-pages) explains how the switch works. + +### Checking your work + +Three commands are worth running before you open a pull request, all from `website/`: + +```bash +npm run typecheck # tsc across website/src/ +npm run build # the documentation site +npm run build:marketing # the marketing site +``` + +`onBrokenLinks` is set to `throw`, so a link to a page that does not exist fails the build rather than shipping a 404. Run both builds even when a change looks like it belongs to one site: `src/theme/`, the emitted stylesheet and `docusaurus.config.ts` are shared, so a change there can break the site you were not looking at. + +The dev server is client-rendered and will not catch either of those. To see what Netlify actually serves, build and serve the output: `npm run serve` for the documentation site, `npm run serve:marketing` for the marketing one. + ## How Overture Docs Works - **Docusaurus**: We use Docusaurus to render the site, providing a sleek and navigable interface for our documentation. @@ -59,6 +79,48 @@ npm start - **Interactive components**: Some pages embed browser-based tooling (the configuration generator, the Lectern dictionary playground, and the Song schema playground) built as React components under `website/src/components/`. +- **Two sites from one repository**: this repository builds both `docs.overture.bio` and the `overture.bio` marketing site, selected by the `OVERTURE_SITE` environment variable. See [The overture.bio marketing pages](#the-overturebio-marketing-pages) and [Deployment](#deployment). + +## The overture.bio marketing pages + +The marketing site was ported into this repository so that one codebase serves both hostnames. The code is complete and both sites build; what is left is the Netlify and DNS work described under [Deployment](#deployment), which is what actually moves visitors from the Gatsby site to this one. + +- **Where the code lives**: everything is under `website/src/marketing/`. Routes are `pages/`, one directory apiece (`collaborate`, `impact`, `privacy`, `products`, `terms-conditions`) plus `pages/index.tsx`, which is the home page. Components, constants, case-study data and stylesheets sit alongside them. + +- **Two builds, one branch**: `docusaurus.config.ts` reads `OVERTURE_SITE`. Unset, it builds the documentation site exactly as before. Set to `marketing`, it builds overture.bio: the documentation plugin instances and the redirect table are dropped, and the pages plugin points at `src/marketing/pages/` so the marketing home is served at `/`. Search stays on both sites, against the same index, with the marketing build configured to send results cross-host. Neither site contains the other's routes, so neither hostname serves a second copy of the other. + +- **The two sites have separate navigation**: the marketing build carries its own navbar and footer so a reader can tell which of the two sites they are on. `website/src/theme/Navbar/` and `website/src/theme/Footer/` pick one using `useIsMarketingSite`, which reads the build mode rather than the route. Adding a marketing nav item means editing `MarketingNavbar.tsx`, not the site config. + +- **Styles are Sass, and they are scoped**: the marketing pages carry their own styling, which would otherwise fight the documentation theme. Every stylesheet is imported inside a `.marketing` block by `website/src/marketing/styles/index.scss`, and only `MarketingPage.tsx` imports that file. If you are adding marketing styles, add a partial and import it there rather than importing a stylesheet from a component. + +- **Assets live in `website/static/img/marketing/`** and are referenced by path, not imported. + +> [!NOTE] +> The staged copy of the Gatsby site that lived here during the port was deleted once the pages were rebuilt. [overture-stack/website](https://github.com/overture-stack/website) still serves overture.bio until DNS is cut over, so until then a change that has to reach the live site goes there, not here. + +## Deployment + +Both sites are Netlify sites building this repository from the same branch. They differ only in one environment variable and the publish directory. + +| | docs.overture.bio | overture.bio | +| --- | --- | --- | +| Base directory | `website` | `website` | +| Build command | `npm ci && npm run build` | `npm ci && npm run build:marketing` | +| Publish directory | `website/build` | `website/build-marketing` | +| `OVERTURE_SITE` | unset | `marketing` | +| `NODE_VERSION` | `20` | `20` | + +Neither site needs a `netlify.toml`, and adding one would be a mistake: a single file on a single branch cannot say different things to two sites. Everything that would go in one is expressed per site instead. + +- **Redirects** live in `website/static-docs/_redirects` and `website/static-marketing/_redirects`. Each is copied into its own build's output by the `staticDirectories` setting, so each host serves only the rules it owes. Netlify combines them with any rules configured in the site UI, applying the UI rules first. +- **`robots.txt`** is per host for the same reason, next to each `_redirects`. +- **`sitemap.xml`** is generated for each build from the `url` in `docusaurus.config.ts`, so it always names the host it is served from. +- **Submodules** are fetched by Netlify automatically: every URL in `.gitmodules` is public HTTPS. The documentation symlinks under `website/docs/` are committed as git symlinks with relative targets, so [symlinker.sh](symlinker.sh) does not need to run during a build. + +Because submodule pointers are pinned commits, a documentation change landed in a component repository does not reach the deployed site until a submodule bump is committed here. + +Running either site locally is covered under [Running it Locally](#running-it-locally). + ## Repository structure ``` @@ -86,9 +148,14 @@ npm start ├── /src/ # Website source code │ ├── /components/ # React components │ ├── /css/ # Component-specific styles + │ ├── /marketing/ # Components, data and Sass for the overture.bio pages │ ├── /theme/ # Global theme configuration and styling - │ └── /pages/ # Static page content + │ │ └── /pages/ # The overture.bio routes; index.tsx is its home page + │ └── /pages/ # Documentation homepage + ├── /static-docs/ # robots.txt and _redirects for docs.overture.bio only + ├── /static-marketing/ # robots.txt and _redirects for overture.bio only ├── /static/ # Static assets served as-is + │ └── /img/marketing/ # Images and icons for the marketing pages ├── docusaurus.config.ts # Site config: plugin instances, navbar, redirects └── *Sidebars.ts # One sidebar file per documentation section ``` @@ -102,6 +169,8 @@ npm start - **/docs/use-docs/**: Task-oriented content for people working with a running platform - **/docs/community-docs/**: Community-focused content, including the org-wide documentation standards linked from the `.github` submodule - **/src/**: Website implementation files including custom components, styling, and page content + - **/src/marketing/**: The overture.bio marketing site, rebuilt from the Gatsby site, routes included + - **/static-docs/**, **/static-marketing/**: The files each host serves alone, `robots.txt` and `_redirects`. See [Deployment](#deployment) > [!IMPORTANT] > Documentation content is owned by the submodules, not by this repository. Where a page under `website/docs/` is a symlink, edit the source file in `submodules//docs/` and land the change through that project's own repository. @@ -118,3 +187,9 @@ npm start > [!TIP] > The Overture Docs repo contains everything, therefore finding & tracking links and content across all our repos has never been easier. + +## License + +Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0), the same license as the rest of the Overture stack. See [LICENSE](LICENSE) for the full text, and the [licensing page](https://docs.overture.bio/community/licensing) for an overview. + +Documentation content vendored from the `submodules/` repositories remains under the license of its own repository. diff --git a/website/docs/community-docs/02-funding.md b/website/docs/community-docs/02-funding.md index 5c9b54ab..fc52dba0 100644 --- a/website/docs/community-docs/02-funding.md +++ b/website/docs/community-docs/02-funding.md @@ -1,46 +1,66 @@ # Funding -We gratefully acknowledge the financial support that has made this project possible. Our work has been funded by the following organizations and grants: +Overture is developed and built by the Genome Informatics program at the Ontario +Institute for Cancer Research, on grants from Canadian and American public +funders. This page lists every grant supporting the platform, its funding +period, and the organizations contributing to individual components. -### Digital Research Alliance of Canada +## Digital Research Alliance of Canada -Under this grant we are transforming Overture from a data catalogue into an AI-assisted discovery and analysis platform. Using the Model Context Protocol (MCP), researchers will be able to explore, analyze, and visualize data through transparent, reproducible conversational workflows across Canadian research platforms including ICGC-ARGO, OHCRN, and iMicroSeq. +**2026 to 2028.** Under this grant we are transforming Overture from a data +catalogue into an AI-assisted discovery and analysis platform. Using the Model +Context Protocol (MCP), researchers will be able to explore, analyze, and +visualize data through transparent, reproducible conversational workflows across +Canadian research platforms including ICGC-ARGO, OHCRN, and iMicroSeq. -- **Funding Period:** 2026–2028 +## Canadian Institutes of Health Research -### Canadian Institutes of Health Research +**2023 to 2028.** As working group 2, we are developing a federated framework +for genomic data access and analysis, specifically building an enhanced Overture +data submission system to facilitate the ingestion, validation, and tracking of +clinical and molecular data into the PCGL platform. -As working group 2, we are developing a federated framework for genomic data access and analysis, specifically building an enhanced Overture data submission system to facilitate the ingestion, validation, and tracking of clinical and molecular data into the PCGL platform. +- Project: [Pan-Canadian Genome Library (PCGL)](https://genomelibrary.ca/) -- **Project Title:** [Pan-Canadian Genome Library (PCGL)](https://genomelibrary.ca/) -- **Funding Period:** 2023-2028 +## National Cancer Institute at the US National Institutes of Health -### National Cancer Institute at the US National Institutes of Health +**2021 to 2026.** Under this grant we are making the Overture platform more +accessible by breaking down barriers to adoption, integrating existing analysis +tools and adding federated search functionality between distributed Overture +instances using standardized data governance frameworks +([GA4GH](https://www.ga4gh.org/)). -Under this grant we are making the Overture platform more accessible by breaking down barriers to adoption, integrating existing analysis tools and adding federated search functionality between distributed Overture instances using standardized data governance frameworks ([GA4GH](https://www.ga4gh.org/)). +- Grant number: #U24CA253529 -- **Grant Number:** #U24CA253529 -- **Funding Period:** 2021-2026 +## Canadian COVID-19 Genomic Data Infrastructure -### Canadian COVID-19 Genomic Data Infrastructure +**2020 to 2024.** The [VirusSeq Data +Portal](https://virusseq-dataportal.ca/), since renamed iMicroSeq, demonstrated +Overture's expansion beyond oncology data management. This funding adapted the +platform to support viral genomic data, and enabled the development, deployment +and ongoing maintenance of Canada's central SARS-CoV-2 sequence repository. -The [VirusSeq Data Portal](https://virusseq-dataportal.ca/) demonstrates Overture's successful expansion beyond oncology data management. This funding facilitated the platform's adaptation to support viral genomic data, while enabling the development, deployment, and ongoing maintenance of Canada's central SARS-CoV-2 sequence repository. +- Project: VirusSeq Data Portal, now iMicroSeq -- **Project Title:** VirusSeq Data Portal -- **Funding Period:** 2020-2024 +## The Government of Ontario -### The Government of Ontario - -The Overture platform is developed and maintained by the Genome Informatics Software Engineering group at the Ontario Institute for Cancer Research (OICR). Our team is partly supported through operational funding from the Government of Ontario via OICR. OICR receives core funding from the Government of Ontario through the Ministry of Colleges and Universities. +Our team is partly supported through operational funding from the Government of +Ontario via OICR, which receives core funding from the Government of Ontario +through the Ministry of Colleges and Universities. ## Additional Support -We also thank the following organizations for their contributions: - -- [**EMBL's European Bioinformatics Institute**](https://www.ebi.ac.uk/) - For contributions to Lectern -- [**The Ferretti Lab at the Research Center of the CHU Sainte-Justine**](https://ferlab.bio/a-propos) - For contributions to Arranger -- [**The South African National Bioinformatics Institute (SANBI)**](https://www.sanbi.ac.za/) - Provided valuable feedback on documentation gaps and identified improvements that significantly enhanced the Overture's platform and user experience. +We also thank the following organizations for their contributions to Overture. -For more information about our funding or to discuss potential collaborations, please email us at contact@overture.bio. +- [EMBL's European Bioinformatics Institute](https://www.ebi.ac.uk/), for + contributions to Lectern. +- [The Ferretti Lab at the Research Center of the CHU + Sainte-Justine](https://ferlab.bio/a-propos), for contributions to Arranger. +- [The South African National Bioinformatics Institute + (SANBI)](https://www.sanbi.ac.za/), for feedback that identified documentation + gaps and improvements to the platform and its user experience. -**Last updated:** 07-22-26 +:::info Talk to us about funding +For more information about our funding, or to discuss a potential collaboration, +email us at **contact@overture.bio** +::: diff --git a/website/docs/community-docs/06-citing-us.md b/website/docs/community-docs/06-citing-us.md index 6abbaf60..84e191cf 100644 --- a/website/docs/community-docs/06-citing-us.md +++ b/website/docs/community-docs/06-citing-us.md @@ -1,39 +1,51 @@ -# Publication & Citation +# How to Cite Us -## About Our Publication +Please cite the Overture paper when you use Overture components in your research, +build a data platform with them, reference the architecture, or compare genomics +data platforms. -Overture has been published in **GigaScience**, Volume 14, 2025, with the article ID giaf038. This publication describes our open-source genomics data platform and its impact on the research community. +## The paper -### Publication Details +**Overture: an open-source genomics data platform.** Published in *GigaScience*, +volume 14, 2025, article giaf038. The paper describes Overture's architecture, +the responsibilities of each component, and the research platforms built on it. -**Title:** Overture: an open-source genomics data platform +[Read the paper](https://doi.org/10.1093/gigascience/giaf038), available under a +[Creative Commons Attribution 4.0 International +licence](https://creativecommons.org/licenses/by/4.0/). -**Journal:** GigaScience +- Journal: GigaScience +- Volume 14, 2025 +- Article ID: giaf038 +- DOI: [10.1093/gigascience/giaf038](https://doi.org/10.1093/gigascience/giaf038) +- Licence: CC BY 4.0 -**Volume:** 14 +## Citation formats -**Year:** 2025 +### Standard -**Article ID:** giaf038 - -**DOI:** [https://doi.org/10.1093/gigascience/giaf038](https://doi.org/10.1093/gigascience/giaf038) - -**License:** Creative Commons Attribution 4.0 International License (CC BY 4.0) - -### Authors +``` +Shiell, M., Bajari, R., Andric, D., et al. Overture: an open-source genomics data platform. +GigaScience 14, giaf038 (2025). https://doi.org/10.1093/gigascience/giaf038 +``` -Mitchell Shiell, Rosi Bajari, Dusan Andric, Jon Eubank, Brandon F Chan, Anders J Richardsson, Azher Ali, Bashar Allabadi, Yelizar Alturmessov, Jared Baker, Ann Catton, Kim Cullion, Daniel DeMaria, Patrick Dos Santos, Henrich Feher, Francois Gerthoffert, Minh Ha, Robin A Haw, Atul Kachru, Alexandru Lepsa, Alexis Li, Rakesh N Mistry, Hardeep K Nahal-Bose, Aleksandra Pejovic, Samantha Rich, Leonardo Rivera, Ciarán Schütte, Edmund Su, Robert Tisma, Jaser Uddin, Chang Wang, Alex N Wilmer, Linda Xiang, Junjun Zhang, Lincoln D Stein, Vincent Ferretti, Mélanie Courtot, Christina K Yung +### APA -## How to Cite Overture +``` +Shiell, M., Bajari, R., Andric, D., Eubank, J., Chan, B. F., Richardsson, A. J., ... Yung, C. K. (2025). +Overture: an open-source genomics data platform. GigaScience, 14, giaf038. +https://doi.org/10.1093/gigascience/giaf038 +``` -### Standard Citation Format +### Vancouver ``` -Shiell, M., Bajari, R., Andric, D., et al. Overture: an open-source genomics data platform. -GigaScience 14, giaf038 (2025). https://doi.org/10.1093/gigascience/giaf038 +Shiell M, Bajari R, Andric D, Eubank J, Chan BF, Richardsson AJ, et al. +Overture: an open-source genomics data platform. GigaScience. 2025;14:giaf038. +Available from: https://doi.org/10.1093/gigascience/giaf038 ``` -### BibTeX Citation +### BibTeX ```bibtex @article{shiell2025overture, @@ -49,27 +61,13 @@ GigaScience 14, giaf038 (2025). https://doi.org/10.1093/gigascience/giaf038 } ``` -### APA Format - -``` -Shiell, M., Bajari, R., Andric, D., Eubank, J., Chan, B. F., Richardsson, A. J., ... Yung, C. K. (2025). -Overture: an open-source genomics data platform. GigaScience, 14, giaf038. -https://doi.org/10.1093/gigascience/giaf038 -``` - -### Vancouver Format - -``` -Shiell M, Bajari R, Andric D, Eubank J, Chan BF, Richardsson AJ, et al. -Overture: an open-source genomics data platform. GigaScience. 2025;14:giaf038. -Available from: https://doi.org/10.1093/gigascience/giaf038 -``` - -## When to Cite - -Please cite our publication when: +## Authors -- Using Overture software components in your research -- Building data platforms with Overture -- Referencing Overture's architecture or methodology -- Comparing genomics data platforms +Mitchell Shiell, Rosi Bajari, Dusan Andric, Jon Eubank, Brandon F Chan, Anders J +Richardsson, Azher Ali, Bashar Allabadi, Yelizar Alturmessov, Jared Baker, Ann +Catton, Kim Cullion, Daniel DeMaria, Patrick Dos Santos, Henrich Feher, Francois +Gerthoffert, Minh Ha, Robin A Haw, Atul Kachru, Alexandru Lepsa, Alexis Li, +Rakesh N Mistry, Hardeep K Nahal-Bose, Aleksandra Pejovic, Samantha Rich, +Leonardo Rivera, Ciarán Schütte, Edmund Su, Robert Tisma, Jaser Uddin, Chang +Wang, Alex N Wilmer, Linda Xiang, Junjun Zhang, Lincoln D Stein, Vincent +Ferretti, Mélanie Courtot, Christina K Yung. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index e652faf6..d33e1b4d 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -1,14 +1,311 @@ import { themes as prismThemes } from "prism-react-renderer"; -import type { Config } from "@docusaurus/types"; +import type { Config, PluginConfig } from "@docusaurus/types"; import type * as Preset from "@docusaurus/preset-classic"; +// This repository builds two sites. `OVERTURE_SITE=marketing` produces +// overture.bio: the pages under src/marketing/pages/, with the marketing home +// at the root. Anything else produces docs.overture.bio, which is what a bare +// `npm start`, a CI check and the documentation site's own Netlify build all +// get, so the documentation site needs no configuration to keep behaving as it +// always has. +// +// The split is by build rather than by route because these are two hostnames: +// one build serving both would put every documentation page on overture.bio +// and every marketing page on docs.overture.bio, leaving search engines two +// copies of each site to choose between. +const isMarketing = process.env.OVERTURE_SITE === "marketing"; + +// The four documentation journeys, one plugin instance each, plus the redirect +// table that keeps their older URLs alive. None of it belongs to the marketing +// build, which has no documentation routes for those redirects to resolve +// against (plugin-client-redirects fails on a `to` it cannot find). +const documentationPlugins: PluginConfig[] = [ + [ + "@docusaurus/plugin-content-docs", + { + id: "community", + // Source dir is "docs/community-docs" for consistency with the other + // journeys (develop, deploy, use), which all live under website/docs/. + path: "docs/community-docs", + routeBasePath: "community", + sidebarPath: require.resolve("./communitySidebars.ts"), + // The documentation-standards content is vendored (.github submodule) + // and may carry dead /guides links (see remark/unlink-legacy-paths.js). + remarkPlugins: [require("./remark/unlink-legacy-paths")], + }, + ], + [ + "@docusaurus/plugin-content-docs", + { + id: "develop", + path: "docs/develop-docs", + routeBasePath: "develop", + sidebarPath: require.resolve("./developSidebars.ts"), + // The per-component docs (Song, Maestro, ...) are vendored from each + // component's own submodule and may carry dead /guides links (see + // remark/unlink-legacy-paths.js). + remarkPlugins: [require("./remark/unlink-legacy-paths")], + }, + ], + [ + "@docusaurus/plugin-content-docs", + { + id: "deploy", + path: "docs/deploy-docs", + routeBasePath: "deploy", + sidebarPath: require.resolve("./deploySidebars.ts"), + }, + ], + [ + "@docusaurus/plugin-content-docs", + { + id: "use", + path: "docs/use-docs", + routeBasePath: "use", + sidebarPath: require.resolve("./useSidebars.ts"), + }, + ], + [ + // Preserves old URLs after the Deploy·Build·Use IA migration so external + // links, bookmarks, and cross-links from vendored component docs (which + // this repo cannot edit) don't 404. + "@docusaurus/plugin-client-redirects", + { + redirects: [ + // Guides -> journeys + { to: "/deploy", from: ["/guides", "/guides/getting-started"] }, + { to: "/deploy/deployment", from: "/guides/deployment-guide" }, + { + to: "/deploy/deployment/keycloak", + from: "/guides/deployment-guide/authorization", + }, + { + to: "/deploy/deployment/file-transfer", + from: [ + "/guides/deployment-guide/data-management-storage", + "/deploy/deployment/data-management-storage", + ], + }, + { + to: "/deploy/deployment/search-portal", + from: "/guides/deployment-guide/search-portal", + }, + // Administration guides moved from Deploy to Use (Administrators); + // the overview content now lives on the main Use overview page. + { + to: "/use", + from: ["/guides/administration-guides", "/deploy/administration"], + }, + // Renamed from "Updating the Data Model" for clarity (it's + // specifically about Song's metadata schemas) + { + to: "/use/administration/building-song-schemas", + from: [ + "/guides/administration-guides/updating-the-data-model", + "/deploy/administration/updating-the-data-model", + "/use/administration/updating-the-data-model", + ], + }, + { + to: "/use/administration/index-mappings", + from: [ + "/guides/administration-guides/index-mappings", + "/deploy/administration/index-mappings", + ], + }, + { + to: "/use/administration/customizing-the-data-portal", + from: [ + "/guides/administration-guides/customizing-the-data-portal", + "/deploy/administration/customizing-the-data-portal", + ], + }, + { to: "/use", from: "/guides/user-guides" }, + { + to: "/use/cli-submissions", + from: "/guides/user-guides/cli-submissions", + }, + { + to: "/use/cli-downloads", + from: "/guides/user-guides/cli-downloads", + }, + { + to: "/develop/api-reference", + from: ["/guides/api-reference", "/build/api-reference"], + }, + // Platform Tools -> Deploy + { to: "/deploy", from: "/docs/platform-tools" }, + { to: "/deploy/prelude", from: "/docs/platform-tools/prelude" }, + // The Quickstart page was retired with the prelude `quickstart` branch + // (2026-07-30). Components now document their own development + // environments; this redirect lands on Prelude, its nearest successor. + { to: "/deploy/prelude", from: "/docs/platform-tools/quickstart" }, + // Docs -> Develop + { + to: "/develop/contributing", + from: ["/docs/contribution", "/build/contributing"], + }, + // Documentation standards moved from Build to Community (the + // /build/documentation-standards -> /community/... redirect is + // handled by createRedirects below, alongside its subpages) + { + to: "/community/documentation-standards/documentation-standards", + from: "/docs/documentation-standards", + }, + // Workshop -> Use + { to: "/use/workshop/prerequisites", from: "/workshop" }, + { + to: "/use/workshop/prerequisites", + from: "/workshop/prerequisites", + }, + { + to: "/use/workshop/running-the-demo", + from: "/workshop/running-the-demo", + }, + { + to: "/use/workshop/architecture", + from: "/workshop/architecture", + }, + { + to: "/use/workshop/data-preparation", + from: "/workshop/data-preparation", + }, + { + to: "/use/workshop/generating-configurations", + from: "/workshop/generating-configurations", + }, + { + to: "/use/workshop/docker-configuration", + from: "/workshop/docker-configuration", + }, + { + to: "/use/workshop/loading-data", + from: "/workshop/loading-data", + }, + { + to: "/use/workshop/troubleshooting", + from: "/workshop/troubleshooting", + }, + { + to: "/use/workshop/portal-customization", + from: "/workshop/portal-customization", + }, + { to: "/use/workshop/next-steps", from: "/workshop/next-steps" }, + { + to: "/use/workshop/extension-task", + from: "/workshop/extension-task", + }, + // Reference folded into Develop (formerly named Build) + { + to: "/develop", + from: ["/docs", "/docs/getting-started", "/build"], + }, + // Arranger docs reshuffle: the "AI and automation" reference page + // became the standalone "Arranger MCP server" page, and the + // migration guides moved under Reference. + { + to: "/develop/Arranger/mcp-server", + from: "/develop/Arranger/reference/ai-and-automation", + }, + { + to: "/develop/Arranger/reference/Migration/v3.1", + from: "/develop/Arranger/migration/v3.1", + }, + // Pages retired by the component usage/ -> reference/ restructure + // that have no like-for-like successor, so createRedirects (which + // only rewrites the /develop, /build, /docs prefix) cannot cover + // them. Each target must be a page that exists in the build -- + // plugin-client-redirects fails on a `to` it cannot resolve. + // + // Maestro V5 does not implement exclusion rules or Slack + // notifications; both pages are now draft: true and absent from + // production builds, so these land on the Reference index. + { + // Trailing slash required: category index routes carry one, and + // the plugin validates `to` against the exact route path. + to: "/develop/Maestro/reference/", + from: [ + "/develop/Maestro/usage/exclusion-rules", + "/develop/Maestro/usage/slack-notifications", + ], + }, + // Song's standalone custom-schemas page is gone; its Options content + // is now the "Schema Options" section of Data Model Management. + { + to: "/develop/Song/Reference/data-model-management", + from: "/develop/Song/custom-schemas", + }, + // Lyric's single placeholder page became Overview + Setup. + { + to: "/develop/Lyric/overview", + from: "/develop/Lyric/lyric", + }, + ], + // Component reference moved from /docs/* to /build/* (Reference folded + // into Build), then from /build/core-software/* to /build/* (the + // "Core Software" dropdown was flattened), then from /build/* to + // /develop/* (Build renamed to Develop); redirect every such page. + createRedirects(existingPath) { + const components = [ + "Lectern", + "Lyric", + "Song", + "Score", + "Maestro", + "Arranger", + "Stage", + ]; + for (const component of components) { + const prefix = `/develop/${component}`; + if ( + existingPath === prefix || + existingPath.startsWith(`${prefix}/`) + ) { + const rest = existingPath.slice("/develop/".length); + return [ + `/docs/core-software/${rest}`, + `/build/core-software/${rest}`, + `/build/${rest}`, + ]; + } + } + // Documentation standards moved from /build/documentation-standards/* + // to /community/documentation-standards/documentation-standards/*. + if ( + existingPath.startsWith( + "/community/documentation-standards/documentation-standards", + ) + ) { + return existingPath.replace( + "/community/documentation-standards/documentation-standards", + "/build/documentation-standards", + ); + } + if (existingPath.startsWith("/community/documentation-standards/")) { + return existingPath.replace( + "/community/documentation-standards/", + "/build/documentation-standards/", + ); + } + return undefined; + }, + }, + ], +]; + const config: Config = { - title: "Overture Docs", - tagline: "Resources for Developers, Operators & Informaticians", + // Layout renders page titles as ` | `, so this is the + // suffix on every tab on the site, and the two sites are named differently. + title: isMarketing ? "Overture" : "Overture Docs", + tagline: isMarketing + ? "Open-source microservices for building research data platforms" + : "Resources for Developers, Operators & Informaticians", favicon: "img/favicon.ico", - // Set the production url of your site here - url: "https://docs.overture.bio/", + // The production url of each site. This is what the generated sitemap.xml + // and every canonical URL are built from, so it has to be the host the build + // is actually served on. + url: isMarketing ? "https://overture.bio" : "https://docs.overture.bio/", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: "/", @@ -20,6 +317,22 @@ const config: Config = { onBrokenLinks: "throw", + // Read by useIsMarketingSite, which is what the swizzled Navbar and Footer + // use to pick which site's chrome to render. This is the only place the + // build mode reaches the browser bundle. + customFields: { + site: isMarketing ? "marketing" : "docs", + }, + + // `static/` is shared. The second directory is what each host owns on its + // own: robots.txt (which must name one canonical host, not both) and, for + // marketing, the _redirects file carrying every URL overture.bio owes from + // the Gatsby site it replaces. + staticDirectories: [ + "static", + isMarketing ? "static-marketing" : "static-docs", + ], + headTags: [ { tagName: "meta", @@ -59,276 +372,37 @@ const config: Config = { plugins: [ "./docsPlugin.ts", [ - "@docusaurus/plugin-content-docs", - { - id: "community", - // Source dir is "docs/community-docs" for consistency with the other - // journeys (develop, deploy, use), which all live under website/docs/. - path: "docs/community-docs", - routeBasePath: "community", - sidebarPath: require.resolve("./communitySidebars.ts"), - // The documentation-standards content is vendored (.github submodule) - // and may carry dead /guides links (see remark/unlink-legacy-paths.js). - remarkPlugins: [require("./remark/unlink-legacy-paths")], - }, - ], - [ - "@docusaurus/plugin-content-docs", + // The marketing pages ported from the Gatsby site are written in Sass. + // `includePaths` and `quietDeps` went with Bulma in rebuild phase 2: there + // is no longer a node_modules stylesheet to resolve by package name, and no + // third-party Sass whose deprecations need silencing. + "docusaurus-plugin-sass", { - id: "develop", - path: "docs/develop-docs", - routeBasePath: "develop", - sidebarPath: require.resolve("./developSidebars.ts"), - // The per-component docs (Song, Maestro, ...) are vendored from each - // component's own submodule and may carry dead /guides links (see - // remark/unlink-legacy-paths.js). - remarkPlugins: [require("./remark/unlink-legacy-paths")], - }, - ], - [ - "@docusaurus/plugin-content-docs", - { - id: "deploy", - path: "docs/deploy-docs", - routeBasePath: "deploy", - sidebarPath: require.resolve("./deploySidebars.ts"), - }, - ], - [ - "@docusaurus/plugin-content-docs", - { - id: "use", - path: "docs/use-docs", - routeBasePath: "use", - sidebarPath: require.resolve("./useSidebars.ts"), - }, - ], - [ - // Preserves old URLs after the Deploy·Build·Use IA migration so external - // links, bookmarks, and cross-links from vendored component docs (which - // this repo cannot edit) don't 404. - "@docusaurus/plugin-client-redirects", - { - redirects: [ - // Guides -> journeys - { to: "/deploy", from: ["/guides", "/guides/getting-started"] }, - { to: "/deploy/deployment", from: "/guides/deployment-guide" }, - { - to: "/deploy/deployment/keycloak", - from: "/guides/deployment-guide/authorization", - }, - { - to: "/deploy/deployment/file-transfer", - from: [ - "/guides/deployment-guide/data-management-storage", - "/deploy/deployment/data-management-storage", - ], - }, - { - to: "/deploy/deployment/search-portal", - from: "/guides/deployment-guide/search-portal", - }, - // Administration guides moved from Deploy to Use (Administrators); - // the overview content now lives on the main Use overview page. - { - to: "/use", - from: ["/guides/administration-guides", "/deploy/administration"], - }, - // Renamed from "Updating the Data Model" for clarity (it's - // specifically about Song's metadata schemas) - { - to: "/use/administration/building-song-schemas", - from: [ - "/guides/administration-guides/updating-the-data-model", - "/deploy/administration/updating-the-data-model", - "/use/administration/updating-the-data-model", - ], - }, - { - to: "/use/administration/index-mappings", - from: [ - "/guides/administration-guides/index-mappings", - "/deploy/administration/index-mappings", - ], - }, - { - to: "/use/administration/customizing-the-data-portal", - from: [ - "/guides/administration-guides/customizing-the-data-portal", - "/deploy/administration/customizing-the-data-portal", - ], - }, - { to: "/use", from: "/guides/user-guides" }, - { - to: "/use/cli-submissions", - from: "/guides/user-guides/cli-submissions", - }, - { - to: "/use/cli-downloads", - from: "/guides/user-guides/cli-downloads", - }, - { - to: "/develop/api-reference", - from: ["/guides/api-reference", "/build/api-reference"], - }, - // Platform Tools -> Deploy - { to: "/deploy", from: "/docs/platform-tools" }, - { to: "/deploy/prelude", from: "/docs/platform-tools/prelude" }, - // The Quickstart page was retired with the prelude `quickstart` branch - // (2026-07-30). Components now document their own development - // environments; this redirect lands on Prelude, its nearest successor. - { to: "/deploy/prelude", from: "/docs/platform-tools/quickstart" }, - // Docs -> Develop - { - to: "/develop/contributing", - from: ["/docs/contribution", "/build/contributing"], - }, - // Documentation standards moved from Build to Community (the - // /build/documentation-standards -> /community/... redirect is - // handled by createRedirects below, alongside its subpages) - { - to: "/community/documentation-standards/documentation-standards", - from: "/docs/documentation-standards", - }, - // Workshop -> Use - { to: "/use/workshop/prerequisites", from: "/workshop" }, - { - to: "/use/workshop/prerequisites", - from: "/workshop/prerequisites", - }, - { - to: "/use/workshop/running-the-demo", - from: "/workshop/running-the-demo", - }, - { - to: "/use/workshop/architecture", - from: "/workshop/architecture", - }, - { - to: "/use/workshop/data-preparation", - from: "/workshop/data-preparation", - }, - { - to: "/use/workshop/generating-configurations", - from: "/workshop/generating-configurations", - }, - { - to: "/use/workshop/docker-configuration", - from: "/workshop/docker-configuration", - }, - { - to: "/use/workshop/loading-data", - from: "/workshop/loading-data", - }, - { - to: "/use/workshop/troubleshooting", - from: "/workshop/troubleshooting", - }, - { - to: "/use/workshop/portal-customization", - from: "/workshop/portal-customization", - }, - { to: "/use/workshop/next-steps", from: "/workshop/next-steps" }, - { - to: "/use/workshop/extension-task", - from: "/workshop/extension-task", - }, - // Reference folded into Develop (formerly named Build) - { - to: "/develop", - from: ["/docs", "/docs/getting-started", "/build"], - }, - // Arranger docs reshuffle: the "AI and automation" reference page - // became the standalone "Arranger MCP server" page, and the - // migration guides moved under Reference. - { - to: "/develop/Arranger/mcp-server", - from: "/develop/Arranger/reference/ai-and-automation", - }, - { - to: "/develop/Arranger/reference/Migration/v3.1", - from: "/develop/Arranger/migration/v3.1", - }, - // Pages retired by the component usage/ -> reference/ restructure - // that have no like-for-like successor, so createRedirects (which - // only rewrites the /develop, /build, /docs prefix) cannot cover - // them. Each target must be a page that exists in the build -- - // plugin-client-redirects fails on a `to` it cannot resolve. - // - // Maestro V5 does not implement exclusion rules or Slack - // notifications; both pages are now draft: true and absent from - // production builds, so these land on the Reference index. - { - // Trailing slash required: category index routes carry one, and - // the plugin validates `to` against the exact route path. - to: "/develop/Maestro/reference/", - from: [ - "/develop/Maestro/usage/exclusion-rules", - "/develop/Maestro/usage/slack-notifications", - ], - }, - // Song's standalone custom-schemas page is gone; its Options content - // is now the "Schema Options" section of Data Model Management. - { - to: "/develop/Song/Reference/data-model-management", - from: "/develop/Song/custom-schemas", - }, - // Lyric's single placeholder page became Overview + Setup. - { - to: "/develop/Lyric/overview", - from: "/develop/Lyric/lyric", - }, - ], - // Component reference moved from /docs/* to /build/* (Reference folded - // into Build), then from /build/core-software/* to /build/* (the - // "Core Software" dropdown was flattened), then from /build/* to - // /develop/* (Build renamed to Develop); redirect every such page. - createRedirects(existingPath) { - const components = [ - "Lectern", - "Lyric", - "Song", - "Score", - "Maestro", - "Arranger", - "Stage", - ]; - for (const component of components) { - const prefix = `/develop/${component}`; - if ( - existingPath === prefix || - existingPath.startsWith(`${prefix}/`) - ) { - const rest = existingPath.slice("/develop/".length); - return [ - `/docs/core-software/${rest}`, - `/build/core-software/${rest}`, - `/build/${rest}`, - ]; - } - } - // Documentation standards moved from /build/documentation-standards/* - // to /community/documentation-standards/documentation-standards/*. - if ( - existingPath.startsWith( - "/community/documentation-standards/documentation-standards", - ) - ) { - return existingPath.replace( - "/community/documentation-standards/documentation-standards", - "/build/documentation-standards", - ); - } - if (existingPath.startsWith("/community/documentation-standards/")) { - return existingPath.replace( - "/community/documentation-standards/", - "/build/documentation-standards/", - ); - } - return undefined; + sassOptions: { + // The marketing styles nest their imports inside `.marketing` to keep + // their bare class names and element rules off the documentation pages, + // which share one emitted stylesheet with them. `@use` cannot be nested + // inside a selector, so `@import` is the only way to express that. + // Removing Bulma did not resolve this: the site's own partials need the + // same scoping, so the Dart Sass 3.0 deadline is still live and now + // needs a different answer. See .dev/roadmap.md. + silenceDeprecations: ["import"], }, }, ], + // Tailwind runs through PostCSS. It is prefixed and preflight-free (see + // src/css/tailwind.css), so unlike Bulma it can be a global stylesheet + // without reaching the documentation pages' typography. + function tailwindPlugin() { + return { + name: "tailwind-plugin", + configurePostCss(postCssOptions: { plugins: unknown[] }) { + postCssOptions.plugins.push(require("@tailwindcss/postcss")); + return postCssOptions; + }, + }; + }, + ...(isMarketing ? [] : documentationPlugins), ], markdown: { @@ -347,20 +421,29 @@ const config: Config = { // lives under the Develop journey (the "develop" plugin-content-docs // instance above, served at /develop/*). docs: false, - blog: { - showReadingTime: true, - feedOptions: { - type: ["rss", "atom"], - xslt: true, - }, - editUrl: "https://github.com/overture-stack/docs", - // Useful options to enforce blogging best practices - onInlineTags: "warn", - onInlineAuthors: "warn", - onUntruncatedBlogPosts: "warn", + // The marketing build has no blog and no documentation: its routes are + // exactly the pages under src/marketing/pages/, whose index.tsx is the + // overture.bio home page. The documentation build keeps `src/pages`, + // where index.tsx is the docs homepage. + pages: { + path: isMarketing ? "src/marketing/pages" : "src/pages", }, + blog: isMarketing + ? false + : { + showReadingTime: true, + feedOptions: { + type: ["rss", "atom"], + xslt: true, + }, + editUrl: "https://github.com/overture-stack/docs", + // Useful options to enforce blogging best practices + onInlineTags: "warn", + onInlineAuthors: "warn", + onUntruncatedBlogPosts: "warn", + }, theme: { - customCss: "./src/css/custom.css", + customCss: ["./src/css/custom.css", "./src/css/tailwind.css"], }, } satisfies Preset.Options, ], @@ -441,13 +524,16 @@ const config: Config = { label: "GitHub", href: "https://github.com/overture-stack", }, + // Cross-host, on the apex rather than www to match the `url` the + // marketing build is generated with. If www is chosen as the + // canonical host instead, these two move with it. { label: "Privacy Statement", - href: "https://www.overture.bio/privacy/", + href: "https://overture.bio/privacy/", }, { label: "Terms & Conditions", - href: "https://www.overture.bio/terms-conditions/", + href: "https://overture.bio/terms-conditions/", }, ], }, @@ -459,12 +545,27 @@ const config: Config = { darkTheme: prismThemes.dracula, additionalLanguages: ["json"], }, + // Both sites search, against the one index, which is crawled from + // docs.overture.bio. MarketingNavbar renders `@theme/SearchBar` just as the + // documentation navbar does, so dropping this config from the marketing + // build does not remove the search box: it leaves the empty fallback + // SearchBar in its place and the box silently disappears. algolia: { // application ID provided by Algolia appId: "3Y4T1NN3P4", // Public API key apiKey: "4fd7bf617c615c6714537dda13d900dd", indexName: "overture", + // Every hit in the index is a docs.overture.bio URL. On the documentation + // site those are same-origin, and the theme strips the origin so results + // route through the SPA router. On overture.bio they are not: without + // this, `https://docs.overture.bio/develop/Song/overview` would be pushed + // as `/develop/Song/overview` into a site with no such route, and every + // result would 404. Matching hits navigate with window.location instead. + // + // The documentation build must NOT set this, or its own results would + // become full page loads rather than client-side navigation. + ...(isMarketing ? { externalUrlRegex: "docs\\.overture\\.bio" } : {}), }, } satisfies Preset.ThemeConfig, }; diff --git a/website/package-lock.json b/website/package-lock.json index 981651b3..bd069d1b 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -21,12 +21,17 @@ "@overture-stack/lectern-dictionary": "^2.0.0", "@overture-stack/lectern-ui": "^1.0.0", "@replit/codemirror-indentation-markers": "^6.5.3", + "@tailwindcss/postcss": "^4.3.3", "@uiw/react-codemirror": "^4.25.11", "clsx": "^2.0.0", + "docusaurus-plugin-sass": "^0.2.6", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "swagger-ui-react": "^5.29.2" + "sass": "^1.102.0", + "simple-icons": "^16.28.0", + "swagger-ui-react": "^5.29.2", + "tailwindcss": "^4.3.3" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.10.2", @@ -286,6 +291,18 @@ "node": ">= 14.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@antfu/install-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", @@ -5227,6 +5244,319 @@ "zod": "^3.23.8" } }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", @@ -6535,34 +6865,311 @@ "integrity": "sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==", "license": "Apache-2.0", "dependencies": { - "apg-lite": "^1.0.3" + "apg-lite": "^1.0.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@swaggerexpert/json-pointer": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", + "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { - "node": ">=12.20.0" + "node": ">=14.0.0" } }, - "node_modules/@swaggerexpert/json-pointer": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", - "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", - "license": "Apache-2.0", - "dependencies": { - "apg-lite": "^1.0.4" - }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.20.0" + "node": ">= 20" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.16" + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, "node_modules/@tanstack/react-table": { @@ -10339,6 +10946,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -10398,6 +11014,19 @@ "node": ">=6" } }, + "node_modules/docusaurus-plugin-sass": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", + "integrity": "sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==", + "license": "MIT", + "dependencies": { + "sass-loader": "^16.0.2" + }, + "peerDependencies": { + "@docusaurus/core": "^2.0.0-beta || ^3.0.0-alpha", + "sass": "^1.30.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -13174,6 +13803,267 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -13330,6 +14220,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-asynchronous": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", @@ -19570,6 +20469,100 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.102.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz", + "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==", + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "license": "MIT" + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", @@ -20073,6 +21066,25 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-icons": { + "version": "16.28.0", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.28.0.tgz", + "integrity": "sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/simple-icons" + }, + { + "type": "github", + "url": "https://github.com/sponsors/simple-icons" + } + ], + "license": "CC0-1.0", + "engines": { + "node": ">=0.12.18" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -20637,6 +21649,12 @@ "react-dom": ">=16.8.0 <20" } }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", diff --git a/website/package.json b/website/package.json index d5caeae8..232c67f8 100644 --- a/website/package.json +++ b/website/package.json @@ -6,6 +6,9 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", + "start:marketing": "OVERTURE_SITE=marketing docusaurus start", + "build:marketing": "OVERTURE_SITE=marketing docusaurus build --out-dir build-marketing", + "serve:marketing": "OVERTURE_SITE=marketing docusaurus serve --dir build-marketing", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -28,12 +31,17 @@ "@overture-stack/lectern-dictionary": "^2.0.0", "@overture-stack/lectern-ui": "^1.0.0", "@replit/codemirror-indentation-markers": "^6.5.3", + "@tailwindcss/postcss": "^4.3.3", "@uiw/react-codemirror": "^4.25.11", "clsx": "^2.0.0", + "docusaurus-plugin-sass": "^0.2.6", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "swagger-ui-react": "^5.29.2" + "sass": "^1.102.0", + "simple-icons": "^16.28.0", + "swagger-ui-react": "^5.29.2", + "tailwindcss": "^4.3.3" }, "overrides": { "@overture-stack/lectern-ui": { diff --git a/website/src/components/MatomoTracking/MatomoTracking.tsx b/website/src/components/MatomoTracking/MatomoTracking.tsx index 8dc67f37..20b4c5ec 100644 --- a/website/src/components/MatomoTracking/MatomoTracking.tsx +++ b/website/src/components/MatomoTracking/MatomoTracking.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import { useLocation } from '@docusaurus/router'; @@ -8,7 +8,7 @@ declare global { } } -export default function MatomoTracking(): JSX.Element | null { +export default function MatomoTracking(): React.JSX.Element | null { const location = useLocation(); useEffect(() => { @@ -16,6 +16,18 @@ export default function MatomoTracking(): JSX.Element | null { return; } + // Production only. Without this, `npm start` and a local `npm run serve` + // report real page views into site 76, recorded against whatever host the + // browser is on, which for a development run is http://localhost. The + // Gatsby site that served overture.bio set `dev: false` on + // gatsby-plugin-matomo for exactly this reason. + // + // Webpack replaces NODE_ENV with a literal, so the production bundle keeps + // the tracking and the development bundle never reaches it. + if (process.env.NODE_ENV !== 'production') { + return; + } + const _paq = (window._paq = window._paq || []); // Initialize Matomo tracking diff --git a/website/src/css/tailwind.css b/website/src/css/tailwind.css new file mode 100644 index 00000000..38c9a592 --- /dev/null +++ b/website/src/css/tailwind.css @@ -0,0 +1,108 @@ +/* + * Tailwind for the marketing pages, added in rebuild phase 2 as Bulma's + * replacement for new work. The layout classes the ported markup already uses + * live in src/marketing/styles/_layout.scss; this is what phase 3's new pages + * are meant to be built with. + * + * Three deliberate choices, and all three are what make it safe to load this + * globally in a build that already has Infima: + * + * 1. `prefix(ow)` namespaces every utility and every custom property, so a + * class is `ow:flex`, not `flex`, and a variable is `--ow-spacing`, not + * `--spacing`. Nothing Tailwind emits can collide with an Infima, + * Docusaurus or marketing class. This is the thing Bulma could not do, and + * the reason its bare `.navbar` and `.button` collided with Infima's. + * + * 2. Preflight is left out. The layer imports below pull in `theme` and + * `utilities` but not `base`, which is where Tailwind's global element + * reset lives. Loading it would restyle every documentation page, since + * Docusaurus emits one shared stylesheet. The marketing pages get their + * reset from `_layout.scss` instead, scoped under `.marketing`. + * + * 3. `source(none)` disables automatic content detection, which would + * otherwise crawl up from this file and scan the whole repository, + * `submodules/` and its eight vendored repos included. The `@source` lines + * below name the two directories that hold page and component code, so + * utilities are generated from those and nothing else. (It also used to + * pick up `website-legacy/`, the staged Gatsby site, which was deleted on + * 2026-08-07; the reason for the flag is unchanged without it.) + * + * A consequence of 2 worth knowing before using this: without preflight there is + * no `box-sizing: border-box` reset and no margin zeroing outside `.marketing`. + * Tailwind utilities used on a documentation page will apply, but they sit on + * Infima's element defaults rather than a clean slate. + * + * A fourth choice, `important` on the utilities import, is about the cascade + * rather than about scoping, and it is not optional here. + * + * A fifth, added in phase 3: the utilities are deliberately NOT in a layer, and + * that is load-bearing. They were in `layer(utilities)` when phase 2 added them, + * which was fine in development and silently wrong in production. Docusaurus's + * production CSS minifier merges every `@media` block and moves the result to + * the end of the file, which lifts Tailwind's responsive variants out of the + * `@layer utilities` block they were emitted inside. Layered and unlayered + * `!important` declarations do not rank by source order (layered wins), so a + * hoisted `.ow\:lg\:flex-row` lost to an in-layer `.ow\:flex-col` no matter how + * wide the window was. Measured, not inferred: `--no-minify` kept both rules in + * the layer and the variant worked, the minified build put them on opposite + * sides of the layer boundary and it did not. + * + * Unlayered, both rules are `!important` and unlayered, so source order decides + * again and Tailwind's own ordering (variants after the utilities they override) + * holds. The layer was never doing anything else: utilities need `important` to + * beat `.marketing X` either way, since `.ow\:py-8` is one class against two. + * `theme` stays layered because it only carries custom properties. + * + * The marketing SCSS is unlayered and nested under `.marketing`, so every one of + * its rules outranks a bare utility twice over: unlayered CSS beats layered CSS + * whatever the specificity, and `.marketing h1` carries a class more than + * `.ow\:py-8` does. Without `important`, utilities silently lose to the reset in + * `_layout.scss` and to any page partial with an opinion. Measured before adding + * it: `ow:py-8` on the privacy `

` computed to 0px, `ow:font-bold` on a + * `` computed to 900, `ow:py-4` on an `
` computed + * to 48px. An `!important` declaration in a layer does beat an unlayered normal + * one, so this is what makes the utilities usable at all. + * + * The layer beneath cannot be moved instead: putting the marketing SCSS in a + * layer would make it lose to Infima, which is unlayered and outside our control. + * + * What this costs, and it is worth knowing before phase 3: an `ow:` utility can + * no longer be overridden by an ordinary SCSS rule. If a partial needs to win + * against one, remove the utility from the markup rather than escalating with a + * second `!important`. + */ + +@layer theme; + +@import "tailwindcss/theme.css" layer(theme) prefix(ow); +@import "tailwindcss/utilities.css" prefix(ow) source(none) important; + +@source "../marketing"; +@source "../pages"; + +/* + * The marketing palette, so phase 3 markup can say `ow:text-navy` instead of + * carrying a hex value in the JSX. + * + * Mirrored by hand from src/marketing/styles/_color-codes.scss, which stays the + * source of truth: the SCSS tree is 2,800 lines and is not moving to Tailwind, + * so a value changes there first and then here. Only the colours the new pages + * actually use are listed, rather than the whole palette, so the two files are + * cheap to keep in step. + * + * The names deliberately avoid Tailwind's own palette (`blue`, `slate`, `gray` + * and the rest). Redefining `--color-blue-800` would silently change what + * `ow:bg-blue-800` means for anyone who assumes Tailwind's default, whereas + * `navy` and `mist` are unambiguous. Theme variables are declared without the + * prefix and emitted with it, as `--ow-color-navy`. + */ +@theme { + --color-navy: #003055; /* $navy-black, the heading and body colour */ + --color-navy-mid: #04518c; /* $navy-dark, buttons and the darker band */ + --color-ink: #5e6068; /* $c-grey-500, secondary text */ + --color-rule: #dfdfe1; /* $grey-3, hairlines between rows */ + --color-mist: #f2f3f5; /* $grey-2, the grey section background */ + --color-pale: #dfecf2; /* $blue-block, the pale blue band */ + --color-link: #0b75a2; /* $blue-links */ + --color-accent: #f2d021; /* $yellow, the rule under a heading */ +} diff --git a/website/src/marketing/MarketingPage.tsx b/website/src/marketing/MarketingPage.tsx new file mode 100644 index 00000000..f48b30b0 --- /dev/null +++ b/website/src/marketing/MarketingPage.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Layout from "@theme/Layout"; +import "./styles/index.scss"; + +/** + * The shell every ported marketing route renders inside. + * + * It replaces the Gatsby site's `Layout` component, `NavBar`, `MegaMenu` and + * `Footer`: docusaurus.config.ts supplies all of those, and the swizzled + * src/theme/Layout mounts Matomo, so `gatsby-plugin-matomo` has no successor to + * port. `` and react-helmet likewise collapse into Layout's own title and + * description props. + * + * `wrapperClassName` puts `.marketing` on the content wrapper only, which is + * what keeps Bulma's resets off the documentation pages in the same build. + */ +export type MarketingPageProps = { + title: string; + description: string; + className: string; + children: React.ReactNode; +}; + +export default function MarketingPage({ + title, + description, + className, + children, +}: MarketingPageProps) { + return ( + +
{children}
+
+ ); +} diff --git a/website/src/marketing/components/Button.tsx b/website/src/marketing/components/Button.tsx new file mode 100644 index 00000000..b6586453 --- /dev/null +++ b/website/src/marketing/components/Button.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Icon } from "./Icon"; +import Link from "./Link"; + +const btnTypes: Record = { + primary: "button is-primary", // uses bulma + secondary: "button is-white secondary", // bulma + custom css + default: "default", +}; + +export type ButtonSize = + | "default" + | "large" + | "medium" + | "navGithub" + | "navSlack" + | "small"; + +const btnSizes: Record = { + // Bulma classes + default: "", + large: "is-large", + medium: "is-medium", + navGithub: "is-medium", + navSlack: "is-medium", + small: "is-small", +}; + +/** + * Icon pixel size for each button size. Typed against the same `ButtonSize` + * union as `btnSizes` (rather than `Record`) so the two stay + * in step: a size missing from either object is a compile error instead of + * an icon silently rendering at `width: undefined`. + */ +const iconSizes: Record = { + default: 16, + large: 32, + medium: 24, + navGithub: 20, + navSlack: 22, + small: 16, +}; + +export type ButtonProps = { + /** Takes priority over `link` when both are passed (`to={anchorLink ?? link}`); the two behave identically today. */ + anchorLink?: string; + /** Required in practice for icon-only buttons, which have no text to read. */ + ariaLabel?: string; + children?: React.ReactNode; + className?: string; + icon?: string; + iconAlt?: string; + iconStyle?: React.CSSProperties; + link?: string; + onClick?: () => void; + size?: ButtonSize; + type?: keyof typeof btnTypes; +}; + +/** + * Component: Button + * Wraps several of Bulma's classes for easier styling, but can be swapped out + * with custom styles (see above objects.) + * + * Example usage: + * + ); + } + + return ( + + + {children} + + ); +} diff --git a/website/src/marketing/components/CardTile.tsx b/website/src/marketing/components/CardTile.tsx new file mode 100644 index 00000000..5ca921d4 --- /dev/null +++ b/website/src/marketing/components/CardTile.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import Link from "./Link"; +import { H3 } from "./Typography"; + +/** + * The white tile the home page's card bands are built from: a title, a body, + * and a "go" line, with the whole tile one link. + * + * Mirrors the documentation site's entry tile (components/SiteMap) — a + * CSS-module component scoped to a page with no marketing styles, so it + * can't be imported here. The two share shape and, where passed, accent + * colour (`--journey-*-color` in css/custom.css). + * + * Renders an `
  • `; the caller supplies the `
      ` and owns the grid. + * `accent` colours the go line (a CSS colour or `var()`); without one it + * uses the site's link blue. + * + * The single anchor is why `children` must be plain paragraphs, not + * interactive content: a link inside a link is invalid and unnavigable, and + * the whole tile is meant to be clickable. + * + * `image` sits inside the anchor, not behind it as an overhang: tiles sit + * two-by-two here, so an overhang on the second row would clash with the + * row above, and every screenshot already has its own chrome/shadow, which + * a second frame would only repeat. Inset on a pale blue field rather than + * bled to the edge for the same reason; see `.CardTile__media` in + * styles/components/_card-tile.scss. + */ +export type CardTileProps = { + title: string; + href: string; + /** The go line's text. The arrow is appended here, not by the caller. */ + linkLabel: string; + accent?: string; + className?: string; + /** Absolute path into static/. Cropped to 16:9, anchored to its own top. */ + image?: string; + /** + * Empty by default, and deliberately: the tile is one link, so its accessible + * name is already the title and the go line, and alt text on the shot would + * only say a third time what the reader is about to open. + */ + imageAlt?: string; + /** A pill in the corner of the shot, for anything that is not live. */ + imageBadge?: string; + children: React.ReactNode; +}; + +export default function CardTile({ + title, + href, + linkLabel, + accent, + className, + image, + imageAlt = "", + imageBadge, + children, +}: CardTileProps) { + return ( +
    • + + {image && ( +
      + {imageAlt} + {imageBadge && ( + {imageBadge} + )} +
      + )} + {/* The padded part. Separate from the anchor so the shot above it can + reach the tile's own edges while the text still holds its inset. */} +
      +

      {title}

      + {children} + {/* `margin-top: auto` in the stylesheet, so this sits on the floor of + every tile whatever the body above it costs. */} + {linkLabel} → +
      + +
    • + ); +} diff --git a/website/src/marketing/components/ComponentTable.tsx b/website/src/marketing/components/ComponentTable.tsx new file mode 100644 index 00000000..4cdddf8e --- /dev/null +++ b/website/src/marketing/components/ComponentTable.tsx @@ -0,0 +1,272 @@ +import React, { useEffect, useState } from "react"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; +import clsx from "clsx"; +import Link from "./Link"; +import { H2 } from "./Typography"; +import { IMPACT_PATH } from "../constants/pages"; +import { + componentIcon, + componentsIn, + groups, + type ComponentGroupId, +} from "../data/components"; +import { DEPLOYMENTS_ANCHOR, usedBy } from "../data/deployments"; +import { floatingTooltipPosition } from "../utils/floatingTooltip"; +import { hoverIntentHandlers } from "../utils/hoverIntent"; + +/** + * The column headers, written once so the real `` row and the row + * repeated at the top of every group (see the note on `ComponentTable` below) + * cannot say two different things. + */ +const COLUMN_HEADERS = ["Component", "What it does", "Documentation", "Used by"]; + +/** + * The whole stack, as one table: comparing two components no longer means + * scrolling past a section boundary. The Collect / Explore / Control + * grouping survives as a group row (``), explained by + * the group blurbs below. + * + * Control's blurb covers today's access delegation (Keycloak); its one row + * is the authorization service being built to replace it — named TBD, + * linked to its repo since there's no documentation yet. + * + * Each icon is the same artwork the home hero's diagram uses + * (data/components.ts owns the path), fitted into a fixed square box since + * they're drawn at different aspect ratios. + * + * A real ``, not divs, since this is tabular data. Fighting Infima's + * global table styles is why the look lives in `_products.scss` rather than + * `ow:` utilities, which are `!important` here and couldn't be overridden + * from SCSS. + * + * Used by is the reverse of /impact/'s Runs column, reading `usedBy` from + * the same `data/deployments.ts` array /impact/ renders from, so a link here + * can never land on a missing row. Text links, not icons, since several + * deployments have no logo. + * + * More than one deployment collapses to a single "N deployments" link + * (Arranger runs in seven) rather than naming each — hover/focus opens a + * tooltip with the full list; clicking links to + * `IMPACT_PATH?used-by={component.id}#{DEPLOYMENTS_ANCHOR}`, which + * impact/index.tsx reads back to highlight every matching row (`:target` + * can only point at one). Exactly one deployment still links straight to + * its own row. + * + * The column headers repeat at the top of every group (`COLUMN_HEADERS`, + * rendered again inside each ``): with only the true `` at the + * very top of the table, scrolling into Explore or Control loses the header + * row entirely and a reader can no longer tell which column is which. + */ +export default function ComponentTable() { + // MDX registers its own anchors for Docusaurus's broken-anchor check; a + // plain React page like this never does, so links to these ids would read + // as broken otherwise. + const brokenLinks = useBrokenLinks(); + + const componentGroups: ComponentGroupId[] = ["collect", "explore", "control"]; + + // The "N deployments" tooltip: the full name list, in the same + // floating-card style /impact/ uses (`floatingTooltipPosition`, shared). + // Only one kind here, so state is just text and the trigger's rect. + const [tooltip, setTooltip] = useState<{ + content: string; + rect: DOMRect; + } | null>(null); + const showTooltip = (content: string) => (event: React.SyntheticEvent) => { + setTooltip({ content, rect: event.currentTarget.getBoundingClientRect() }); + }; + const hideTooltip = () => setTooltip(null); + + // The component id to highlight on arrival, from /impact/'s Runs column + // linking here as `?highlight={id}#{id}` — the mirror of this table's own + // `used-by` link. Read in an effect, not during render, so initial markup + // is unaffected. + // + // A row here is never one of several, so the fragment alone already + // scrolls to it; this state adds the persistent highlight /impact/'s own + // rows get, instead of just the brief `:target` flash. + const [highlightedComponentId, setHighlightedComponentId] = useState< + string | null + >(null); + useEffect(() => { + const componentId = new URLSearchParams(window.location.search).get( + "highlight", + ); + if (componentId) setHighlightedComponentId(componentId); + }, []); + + // Clears the highlight on a click outside the highlighted row, the same + // rule impact/index.tsx uses for its own `highlightedComponent` — see the + // note there. + useEffect(() => { + if (!highlightedComponentId) return; + const clearOnOutsideClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest(".ProductsTable__highlight")) { + setHighlightedComponentId(null); + } + }; + document.addEventListener("click", clearOnOutsideClick); + return () => document.removeEventListener("click", clearOnOutsideClick); + }, [highlightedComponentId]); + + return ( +
      +
      +
      + + + {COLUMN_HEADERS.map((label) => ( + + ))} + + + + {componentGroups.map((groupId) => { + const group = groups.find((candidate) => candidate.id === groupId)!; + brokenLinks.collectAnchor(group.id); + + return ( + + + + + + {/* Repeats the real `` row's words at the top of every + group; see the note on the component above. `th scope="col"` + here too, since these are column headers for the rows below + them and not row headers. */} + + {COLUMN_HEADERS.map((label) => ( + + ))} + + + {componentsIn(groupId).map((component) => { + // The id is the anchor: /products/#song and its siblings are + // linked from the home hero's diagram and predate this page. + brokenLinks.collectAnchor(component.id); + const deployments = usedBy(component.id); + return ( + + + + + + + ); + })} + + ); + })} +
      + {label} +
      + + {group.title} + + + {group.blurb} + + {group.link && ( + + {group.link.label} + + )} +
      + {label} +
      + {/* The flex row is a div inside the cell rather than + the cell itself: `display: flex` on a `th` stops it + being a table cell at all, and the browser wraps an + anonymous one around it. */} +
      + + + + {component.name} + + {component.codename && ( + + {component.codename} + + )} + +
      +
      {component.summary} + {/* The visible words are enough beside the row's own + header, but a link read out of context is not, so + the accessible name carries the component too. */} + + {component.linkLabel ?? "Documentation"} + + + {deployments.length === 0 && "—"} + {deployments.length === 1 && ( + + {deployments[0].name} + + )} + {deployments.length > 1 && ( + d.name).join(", "), + ), + hideTooltip, + )} + > + {deployments.length} deployments + + )} +
      + + + {tooltip && ( + + )} + + ); +} diff --git a/website/src/marketing/components/Hero.tsx b/website/src/marketing/components/Hero.tsx new file mode 100644 index 00000000..20d92d14 --- /dev/null +++ b/website/src/marketing/components/Hero.tsx @@ -0,0 +1,25 @@ +/** + * Commonly used site banner that displays + * text describing the page, and an image. + **/ + +import React from "react"; +import { H1, P3 } from "./Typography"; + +export type HeroProps = { + title: React.ReactNode; + subtitle?: React.ReactNode; +}; + +export default function Hero({ title, subtitle }: HeroProps) { + return ( +
      +
      +
      +

      {title}

      + {subtitle} +
      +
      +
      + ); +} diff --git a/website/src/marketing/components/HeroDiagram.tsx b/website/src/marketing/components/HeroDiagram.tsx new file mode 100644 index 00000000..c738c79f --- /dev/null +++ b/website/src/marketing/components/HeroDiagram.tsx @@ -0,0 +1,167 @@ +import React, { useState } from "react"; +import clsx from "clsx"; +import Link from "./Link"; +import { + heroDiagramHotspots, + type HeroDiagramHotspot, +} from "../data/heroDiagram"; +import { componentUsage } from "../data/componentUsage"; +import { useComponentHighlight } from "../context/ComponentHighlightContext"; +import { floatingTooltipPosition } from "../utils/floatingTooltip"; +import { hoverIntentHandlers } from "../utils/hoverIntent"; + +const PORTAL = "/img/marketing/home/portal.svg"; + +/** + * The home hero's diagram: an orbit of Overture's seven components plus + * Control (in development), positioned to match the reference artwork + * rather than evenly spaced. Each icon is a focusable/hoverable hotspot + * linking to `/products/#` with a name/codename/summary tooltip. + * + * Who runs Overture used to live here too, as a second carousel slide; it's + * now LogoCarousel, its own scrolling section right below the hero. + * + * Hovering a hotspot filters LogoCarousel below to platforms using that + * component; hovering a platform there highlights its components back here + * (both via `data/componentUsage.ts` and shared `ComponentHighlightContext`). + * Every hotspot is always on screen, so this direction just dims the rest + * rather than filtering. + * + * The tooltip is `position: fixed`, positioned in JS from the hovered + * hotspot's `getBoundingClientRect()` (utils/floatingTooltip.ts) rather than + * `position: absolute`: an absolute tooltip could cover the rest of the + * diagram, or run off-window near a shorter viewport's top edge. + * + * Rendered only from `tablet-up` (see _home.scss), hidden with + * `display: none` so nothing here sits in the tab order when the artwork + * isn't shown. + */ +export default function HeroDiagram() { + const { setHighlightedComponent, highlightedPlatform } = + useComponentHighlight(); + const [tooltip, setTooltip] = useState<{ + hotspot: HeroDiagramHotspot; + rect: DOMRect; + } | null>(null); + + return ( +
      +
      +
      + +
      + +
      + + {heroDiagramHotspots.map((hotspot) => { + const usedByHoveredPlatform = highlightedPlatform + ? (componentUsage[highlightedPlatform]?.includes(hotspot.id) ?? false) + : false; + const style: React.CSSProperties = { + left: `${hotspot.left}%`, + top: `${hotspot.top}%`, + width: `${hotspot.width}%`, + height: `${hotspot.height}%`, + }; + const icon = ( + + ); + + // Pointer events, not onMouseEnter/onMouseLeave: @docusaurus/Link + // overwrites `onMouseEnter` internally (for hover-preload), silently + // discarding any passed in. Pointer events are untouched by it. + const showTooltip = (event: React.SyntheticEvent) => { + setHighlightedComponent(hotspot.id); + setTooltip({ + hotspot, + rect: event.currentTarget.getBoundingClientRect(), + }); + }; + const hideTooltip = () => { + setHighlightedComponent(null); + setTooltip(null); + }; + const highlightHandlers = hoverIntentHandlers(showTooltip, hideTooltip); + const platformHighlightClassName = clsx( + "HeroDiagram__hotspot", + usedByHoveredPlatform && "HeroDiagram__hotspot--highlighted", + highlightedPlatform && + !usedByHoveredPlatform && + "HeroDiagram__hotspot--dimmed", + ); + + if (hotspot.comingSoon || !hotspot.href) { + return ( + + {icon} + + ); + } + + return ( + + {icon} + + ); + })} + + {tooltip && ( +
      + + {tooltip.hotspot.name} + {tooltip.hotspot.codename ? ` (${tooltip.hotspot.codename})` : ""} + + + {tooltip.hotspot.summary} + +
      + )} +
      + ); +} diff --git a/website/src/marketing/components/HomeCapabilities.tsx b/website/src/marketing/components/HomeCapabilities.tsx new file mode 100644 index 00000000..2a77e269 --- /dev/null +++ b/website/src/marketing/components/HomeCapabilities.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import CardTile from "./CardTile"; +import { H2, P1 } from "./Typography"; +import { capabilities } from "../data/capabilities"; +import { platforms } from "../data/platforms"; +import { IMPACT_PATH } from "../constants/pages"; + +/** + * "What you can do with Overture": four capability cards, each opening with a + * screenshot as proof it's a running system. Copy is data/capabilities.ts; + * the platform, name, launch year and screenshot each card cites are + * resolved by id from data/platforms.ts, so none of it can drift from + * /impact/. The fourth card has no real deployment behind it and carries a + * labelled mockup instead. + * + * Uses `CardTile`, the same tile `HomeDocs` uses below, with the whole card + * as one link: the four claims vary by up to five lines, and a per-card box + * keeps each link flush with its own floor rather than trailing the + * sentence above it. + */ +export default function HomeCapabilities() { + return ( + // White, as the first band under the blue carousel; the grey went to + // `HomeCollaborate` below when the two swapped, so colours stayed put. + // Tiles take that grey back via `--card-tile-bg` in pages/_home.scss. +
      +
      +

      + What you can do with Overture +

      + {/* Same bar under the same size of heading as "What we do" in + HomeCollaborate: the two bands open identically. */} +
      + + Modular, flexible and reusable components, built for problems of all + shapes and sizes. + + + {/* Four cards on a two-by-two grid, the same `CardTile` the + documentation band below is built from. These were four columns + separated by a yellow rule; the tile's edges do that job now, + plus a link line flush with every card's floor rather than + wherever the outcome sentence ends. */} +
        + {capabilities.map((capability) => { + const platform = platforms.find( + (entry) => entry.id === capability.platform, + ); + + // `?highlight={id}#{id}`, not `platform.href`'s bare fragment: + // the same param partnerLogos.ts uses for LogoCarousel, so a + // card here gets the persistent `ImpactTable__highlight`, not + // just a `:target` flash. + const href = + capability.href ?? + (platform + ? `${IMPACT_PATH}?highlight=${platform.id}#${platform.id}` + : IMPACT_PATH); + + return ( + +

        {capability.outcome}

        + {capability.note && ( +

        {capability.note}

        + )} +
        + ); + })} +
      +
      +
      + ); +} diff --git a/website/src/marketing/components/HomeCollaborate.tsx b/website/src/marketing/components/HomeCollaborate.tsx new file mode 100644 index 00000000..a162d6d9 --- /dev/null +++ b/website/src/marketing/components/HomeCollaborate.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import CardTile from "./CardTile"; +import { Icon } from "./Icon"; +import Link from "./Link"; +import { H2, H3 } from "./Typography"; +import { offers } from "../data/collaboration"; +import metrics from "../data/metrics"; +import { COLLABORATE_PATH, IMPACT_PATH } from "../constants/pages"; +import { + GIGASCIENCE_PAPER_LINK, + GI_PROGRAM_LINK, + OVERTURE_DOCUMENTATION_TEAM_LINK, + OVERTURE_GITHUB_LINK, +} from "../constants/externalLinks"; + +/** + * "What we do": who builds Overture, how, and what to contact us about. Sits + * behind `HomeCapabilities` (what before who), and ahead of the + * documentation/quickstart bands as the staffed route out of the page. + * + * The only place the retired /about-us/ page's material exists now, + * condensed into three paragraphs and a four-item list. The footer's Our + * story and the /about-us/ 301 both land on the H2 below via its `id` + * (`ABOUT_BAND` in constants/pages.ts), which is load-bearing, not just an + * aria target. + * + * Opens on what Overture is for, crediting the team second — the heading + * order asks for the portfolio answer first, who-builds-it as a byline + * under it. + * + * The three offers are `offers` in data/collaboration.ts, the same array + * /collaborate/ renders from (reading `oneLine` here), so the two pages + * can't drift apart. Under their own subhead since the band opens on prose + * about the program, not the offers. + * + * No "Our funders / Publications / Who builds Overture" link row at the + * foot: the funder claim survives as the public-funding sentence below, and + * the three routes are in the footer. + */ +/** + * The four claims about how the software is built, each with its + * /about-us/ badge. `icon` is a key in Icon's own map, not a path. Local + * here rather than in a data file, since /about-us/ (the only other reader) + * is gone. + */ +const values = [ + { + name: "Open source", + text: "Freely available, and open to contributions from the community.", + icon: "aboutUsOpenSource", + }, + { + name: "Modular", + text: "Each component has one responsibility and integrates with the rest.", + icon: "aboutUsModular", + }, + { + name: "Extensible", + text: "Expandable and customizable to what a project actually needs.", + icon: "aboutUsExtensible", + }, + { + name: "Reusable", + text: "Designed to carry across contexts with minimal modification.", + icon: "aboutUsReusable", + }, +]; + +export default function HomeCollaborate() { + // One offer carries a free route; the line under the cards is composed from + // whichever one that is rather than hard-coded to technical support. + const selfServe = offers.find((offer) => offer.selfServe)?.selfServe; + + return ( + // Grey, which came over from `HomeCapabilities` when the two swapped so that + // the page still runs white then grey below the carousel whatever is in the + // two slots. Its tiles stay white on the fallback in + // components/_card-tile.scss: every card band on this page gets a tile that + // is whatever colour its band is not. +
      +
      +
      +

      + What we do with Overture +

      +
      + {/* No `P1`: all three paragraphs share one class, the smallest + prose size this site sets, so the opening statement isn't + visibly bigger than the notes under it. Rules in + pages/_home.scss `&__prose`. */} +

      + Our ability to build{" "} + a diverse portfolio of data platforms{" "} + rests on the continuous development and maintenance of Overture. + Developed at OICR and{" "} + released as open source, + Overture lets our engineers and teams elsewhere build systems that + help researchers and consortiums across disciplines collect, + organize and share their research data. +

      + {/* The one credential on this page: a sentence stating the platform + has been peer reviewed, since the footer's "How to cite us" link + is about citing, not reviewing. `/impact/` carries the other + four papers. */} +

      + The platform is described in{" "} + GigaScience (2025), peer + reviewed and open access. +

      +
      + + {/* The values grid from /about-us/, one line each rather than a card + each — at card size these four claims read as a second page + opening inside this band. Badges are /about-us/'s own, carried + over rather than redrawn. `alt=""` on each: it sits beside the + word it illustrates, so a screen reader reading it twice is worse + than not reading it. */} +
        + {values.map((value) => ( +
      • + + {value.name} + {value.text} +
      • + ))} +
      + +

      + How we collaborate +

      + + {/* The same `CardTile` the documentation band below is built from. + These were bare text columns with a link under each — the tile + adds edges marking where one offer stops and the next starts, and + keeps all three link lines flush regardless of the sentence + above. */} +
        + {offers.map((offer) => ( + // Into the section for this offer, not the top of the page — + // /collaborate/ gives each an id, so three cards feel like one + // page rather than a detour. + +

        {offer.oneLine}

        +
        + ))} +
      + + {/* The self-serve route, under the cards rather than inside one: a + tile is a single link end to end, so a second link inside it + isn't valid or reachable. Keeps this honest that not everything + needs a conversation first. The URL comes from + data/collaboration.ts; the wording is this sentence's own since + `selfServe.label` is written to stand alone, not mid-sentence. */} + {selfServe && ( +

      + Most questions do not need any of this,{" "} + our community support forum is free + and public. +

      + )} +
      +
      + ); +} diff --git a/website/src/marketing/components/HomeDocs.tsx b/website/src/marketing/components/HomeDocs.tsx new file mode 100644 index 00000000..5eb14b94 --- /dev/null +++ b/website/src/marketing/components/HomeDocs.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; +import CardTile from "./CardTile"; +import { H2, P1 } from "./Typography"; +import { docJourneys } from "../data/docJourneys"; + +/** + * The documentation band: one card per hands-on journey. + * + * Cards are `CardTile`, shared with `HomeCapabilities` above. Journey + * colours (`--journey-*-color` in css/custom.css) are shared outright with + * the documentation site, so a "Browse Deploy" line is the same teal on both. + * + * No deep links inside the cards and no "All documentation" button under + * them: three named destinations is the whole offer, and a card with four + * links no longer reads as one click. Those links are all one click + * further in, from the journey landing page each card opens. + * + * No Algolia search here: the navbar's sticky one is already in reach, and + * two search boxes on one page would split one affordance rather than add + * a second. + * + * Each card's "who it is for" line is quoted from that journey's landing + * page (data/docJourneys.ts names the files to check). Links are cross-host + * docs.overture.bio URLs onBrokenLinks can't validate, so they go through + * the URL-check script instead. + */ +export default function HomeDocs() { + // The hero's second button links in-page to `#docs`; a plain React page + // registers no anchors on its own for Docusaurus's broken-anchor check, + // so this does it by hand (same as HomeQuickstart). + useBrokenLinks().collectAnchor("docs"); + + return ( +
      +
      + {/* Heading, bar and lede match `HomeCapabilities` above and "What we + do" before it: the three bands open identically. */} +

      + Getting hands on +

      +
      + + The documentation is organized by what you are doing rather than by + what we built, so each journey starts from a different question. + + +
        + {docJourneys.map((journey) => ( + // The journey ids are the same three the documentation site names + // its colours after, so the accent is composed rather than mapped + // here: a fourth journey would arrive with its colour already set. + +

        {journey.audience}

        +
        + ))} +
      +
      +
      + ); +} diff --git a/website/src/marketing/components/HomeQuickstart.tsx b/website/src/marketing/components/HomeQuickstart.tsx new file mode 100644 index 00000000..8f871bac --- /dev/null +++ b/website/src/marketing/components/HomeQuickstart.tsx @@ -0,0 +1,162 @@ +import React from "react"; +import useBrokenLinks from "@docusaurus/useBrokenLinks"; +import Button from "./Button"; +import { Icon } from "./Icon"; +import Link from "./Link"; +import Terminal from "./Terminal"; +import { H2, H3, P1 } from "./Typography"; +import { + quickstartClaim, + quickstartPortalUrl, + quickstartSteps, +} from "../data/quickstart"; +import { WORKSHOP_DOCS_LINK } from "../constants/externalLinks"; + +const PORTAL_SCREENSHOT = "/img/marketing/home/overtureQuickstartPortal.webp"; + +/** + * The quickstart, at the foot of the home page: an introduction, a blue band + * under it, and the portal screenshot straddling the seam between the two. + * The introduction carries the documentation band's own grey rather than + * white, so that seam is the section's only colour change. + * + * Commands are mirrored from the documentation, not rewritten — + * data/quickstart.ts names the files to check when the demo changes, and + * derives the "3 steps, 2 commands" claim from the steps themselves so the + * subtitle can't fall behind. + * + * Last on the page deliberately: the hero's first button points here, so + * the page's first CTA and last section are the same offer, and scrolling + * the whole way lands on something runnable. + */ +export default function HomeQuickstart() { + // The hero's button links in-page to `#quickstart`; a plain React page + // registers no anchors on its own for Docusaurus's broken-anchor check, + // so this does it by hand (same as HomeDocs). + useBrokenLinks().collectAnchor("quickstart"); + + return ( +
      + {/* Same grey as the documentation band above, so the two read as one + surface and the section's only colour break is the blue band below, + where the screenshot crosses it. */} +
      +
      +

      Getting Started

      +

      + {quickstartClaim.steps} Steps,{" "} + {quickstartClaim.commands} Commands,{" "} + {quickstartClaim.platforms} Platform +

      + + The Overture Quickstart enables a fast and frictionless setup of our + data platform locally. + +
      +
      + +
      + {/* Decorative (`alt=""`): what the portal looks like is the payoff, + not a step. First in source order since it visually joins the two + halves of the section; a screen reader reading an empty image + first costs nothing. Outside `.container`: it's positioned + against the blue band's own top edge, avoiding having to cancel + `.container`'s padding. See _quickstart.scss. */} +
      + +
      + +
      +
      +
        + {quickstartSteps.map((step, index) => ( +
      1. +

        + + + {step.title} + {step.titleLink && ( + <> + {" "} + + {step.titleLink.label} + + + )} + +

        + + {step.note && ( +

        {step.note}

        + )} + + {step.settings && ( +
        + +
        + {step.settings.map((setting) => ( +
        +
        {setting.label}
        +
        + {setting.value} +
        +
        + ))} +
        +
        + )} + + {step.footnote && ( +

        {step.footnote}

        + )} + + {step.commands && ( + + )} +
      2. + ))} +
      + +

      + Your portal will now be accessible from your{" "} + {quickstartPortalUrl}. +

      + +
      + +
      +
      +
      +
      +
      + ); +} diff --git a/website/src/marketing/components/Icon.tsx b/website/src/marketing/components/Icon.tsx new file mode 100644 index 00000000..8cbd4869 --- /dev/null +++ b/website/src/marketing/components/Icon.tsx @@ -0,0 +1,149 @@ +import React from "react"; + +/** + * Every icon the marketing pages can render, by name. + * + * Docusaurus hands SVG imports to SVGR, which returns a React component + * rather than a URL, so these are addressed by path into + * `static/img/marketing/icons/` instead of imported as modules. + */ +const icons: Record = { + aboutUsExtensible: "/img/marketing/icons/aboutUsExtensible.svg", + aboutUsModular: "/img/marketing/icons/aboutUsModule.svg", + aboutUsOpenSource: "/img/marketing/icons/aboutUsOpenSource.svg", + aboutUsReusable: "/img/marketing/icons/aboutUsReusable.svg", + arrowDownNavbar: "/img/marketing/icons/arrow_down_navbar.svg", + arrowDown: "/img/marketing/icons/arrow_down.svg", + arrowLeftBlack: "/img/marketing/icons/arrow-left-black.svg", + arrowLeftBlue: "/img/marketing/icons/arrow-left-blue.svg", + arrowLeftRound: "/img/marketing/icons/arrow_left_round.svg", + arrowRight: "/img/marketing/icons/arrow_right.svg", + arrowRightBlack: "/img/marketing/icons/arrow-right-black.svg", + arrowRightMagenta: "/img/marketing/icons/arrow_right_magenta.svg", + arrowRightRound: "/img/marketing/icons/arrow_right_round.svg", + arrowsRight: "/img/marketing/icons/arrows_right.svg", + assistance: "/img/marketing/icons/icon_assistance.svg", + barGraph: "/img/marketing/icons/bar_graph.svg", + bigData: "/img/marketing/icons/big-data.svg", + cardStack: "/img/marketing/icons/card_stack.svg", + checkmark: "/img/marketing/icons/checkmark.svg", + chevronGrey: "/img/marketing/icons/chevron-grey.svg", + chevronMagenta: "/img/marketing/icons/chevron-magenta.svg", + cloudWeather: "/img/marketing/icons/cloud-weather.svg", + cloud: "/img/marketing/icons/cloud.svg", + cloudSquare: "/img/marketing/icons/cloud_square.svg", + cog: "/img/marketing/icons/cog.svg", + copy: "/img/marketing/icons/copy.svg", + customizable: "/img/marketing/icons/customizable.svg", + data: "/img/marketing/icons/data.svg", + database: "/img/marketing/icons/database.svg", + dna: "/img/marketing/icons/dna.svg", + download: "/img/marketing/icons/download.svg", + emailUs: "/img/marketing/icons/email-us.svg", + exploreDocs: "/img/marketing/icons/explore-docs.svg", + extensible: "/img/marketing/icons/extensible.svg", + feedback: "/img/marketing/icons/icon_feedback.svg", + fingerSnap: "/img/marketing/icons/finger_snap.svg", + githubGrey: "/img/marketing/icons/github_grey.svg", + githubMagenta: "/img/marketing/icons/github_magenta.svg", + githubWhite: "/img/marketing/icons/github_white.svg", + githubYellow: "/img/marketing/icons/github_yellow.svg", + githubFindUs: "/img/marketing/icons/github-find-us.svg", + graphCycle: "/img/marketing/icons/graph_cycle.svg", + gridGlass: "/img/marketing/icons/grid_magnifying_glass.svg", + history: "/img/marketing/icons/history.svg", + identifyNeeds: "/img/marketing/icons/identify-needs.svg", + joinCommunity: "/img/marketing/icons/join-community.svg", + kafka: "/img/marketing/icons/kafka.svg", + lock: "/img/marketing/icons/lock.svg", + lockCode: "/img/marketing/icons/lock_code.svg", + magnify: "/img/marketing/icons/magnify.svg", + mail: "/img/marketing/icons/mail.png", + notes: "/img/marketing/icons/notes.svg", + notes2: "/img/marketing/icons/notes2.svg", + pageWhite: "/img/marketing/icons/page_white.svg", + palette: "/img/marketing/icons/palette.svg", + play: "/img/marketing/icons/play.svg", + power: "/img/marketing/icons/power.svg", + productAdministrativeUI: "/img/marketing/icons/productAdministrativeUI.svg", + productArranger: "/img/marketing/icons/productArranger.svg", + productArrangerWhite: "/img/marketing/icons/productArrangerWhite.svg", + productBilling: "/img/marketing/icons/productBilling.svg", + productBillingWhite: "/img/marketing/icons/productBillingWhite.svg", + productBuiltInUIComponents: "/img/marketing/icons/productBuiltInUIComponents.svg", + productCloudSupport: "/img/marketing/icons/productCloudSupport.svg", + productDMS: "/img/marketing/icons/productDMS.svg", + productDMSWhite: "/img/marketing/icons/productDMSWhite.svg", + productEgo: "/img/marketing/icons/productEgo.svg", + productEgoWhite: "/img/marketing/icons/productEgoWhite.svg", + productEnrolment: "/img/marketing/icons/productEnrolment.svg", + productEnrolmentWhite: "/img/marketing/icons/productEnrolmentWhite.svg", + productHighTransfer: "/img/marketing/icons/productHighTransfer.svg", + productJukebox: "/img/marketing/icons/productJukebox.svg", + productJukeboxWhite: "/img/marketing/icons/productJukeboxWhite.svg", + productMaestro: "/img/marketing/icons/productMaestro.svg", + productMaestroWhite: "/img/marketing/icons/productMaestroWhite.svg", + productMetadataTracking: "/img/marketing/icons/productMetadataTracking.svg", + productMetadataValidation: "/img/marketing/icons/productMetadataValidation.svg", + productMultipleIndexLevels: "/img/marketing/icons/productMultipleIndexLevels.svg", + productMultipleSongsIndex: "/img/marketing/icons/productMultipleSongsIndex.svg", + productOnco: "/img/marketing/icons/productOnco.svg", + productOncoWhite: "/img/marketing/icons/productOncoWhite.svg", + productPersona: "/img/marketing/icons/productPersona.svg", + productPersonaWhite: "/img/marketing/icons/productPersonaWhite.svg", + productRiff: "/img/marketing/icons/productRiff.svg", + productRiffWhite: "/img/marketing/icons/productRiffWhite.svg", + productSamtools: "/img/marketing/icons/productSamtools.svg", + productScalable: "/img/marketing/icons/productScalable.svg", + productScore: "/img/marketing/icons/productScore.svg", + productScoreWhite: "/img/marketing/icons/productScoreWhite.svg", + productSearchAPI: "/img/marketing/icons/productSearchAPI.svg", + productSingleSignOn: "/img/marketing/icons/productSingleSignOn.svg", + productSlackIntegration: "/img/marketing/icons/productSlackIntegration.svg", + productSong: "/img/marketing/icons/productSong.svg", + productSongWhite: "/img/marketing/icons/productSongWhite.svg", + productStateControls: "/img/marketing/icons/productStateControls.svg", + productStateless: "/img/marketing/icons/productStateless.svg", + rocketWhite: "/img/marketing/icons/rocket_white.svg", + search: "/img/marketing/icons/search.svg", + searchBar: "/img/marketing/icons/searchBar.svg", + security: "/img/marketing/icons/security.svg", + share: "/img/marketing/icons/share.svg", + shield: "/img/marketing/icons/shield.svg", + slack: "/img/marketing/icons/icon_slack.svg", + slackJoin: "/img/marketing/icons/slack-join.svg", + smileyFace: "/img/marketing/icons/smileyFace.svg", + slackNew: "/img/marketing/icons/icon_slack_new.svg", + softwareEngineers: "/img/marketing/icons/software-engineers.svg", + spiral: "/img/marketing/icons/spiral.svg", + star: "/img/marketing/icons/star.svg", + support: "/img/marketing/icons/icon_support.svg", + target: "/img/marketing/icons/target.svg", + unsmileyFace: "/img/marketing/icons/unsmileyFace.svg", + user: "/img/marketing/icons/user.svg", + vennDiagram: "/img/marketing/icons/venn_diagram.svg", + xGrey: "/img/marketing/icons/x-grey.svg", +}; + +export type IconProps = { + /** + * Required, so a decorative icon has to say so with `alt=""` rather than + * silently shipping an unlabelled image. + */ + alt: string; + img: string; + size?: number | string; + className?: string; + style?: React.CSSProperties; +}; + +export function Icon({ alt, img, size, className, style }: IconProps) { + return ( + {alt} + ); +} diff --git a/website/src/marketing/components/Link.tsx b/website/src/marketing/components/Link.tsx new file mode 100644 index 00000000..4cc53330 --- /dev/null +++ b/website/src/marketing/components/Link.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import DocusaurusLink from "@docusaurus/Link"; + +/** + * A thin wrapper around Docusaurus's own `Link`, which already handles + * internal routing and same-page hash targets. The one thing it doesn't do — + * opening external links safely in a new tab — is all this adds. + */ +export type LinkProps = React.ComponentProps & { + to?: string; +}; + +export default function Link({ to = "", children, ...props }: LinkProps) { + const isExternal = /^\b(http|mailto)/.test(to); + + if (isExternal) { + return ( + + {children} + + ); + } + + if (!to) { + return {children}; + } + + // A hash on its own is a target on the page already open, so hand it to the + // browser rather than to the router: react-router resolves a bare hash + // against the current path and pushes a new history entry, which lands on the + // same page without scrolling anywhere. Docusaurus's Link is for changing + // route, and this is not one. (The home page's "Get Started" button is the + // first thing here to need it.) + if (to.startsWith("#")) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/website/src/marketing/components/LogoCarousel.tsx b/website/src/marketing/components/LogoCarousel.tsx new file mode 100644 index 00000000..852075ac --- /dev/null +++ b/website/src/marketing/components/LogoCarousel.tsx @@ -0,0 +1,494 @@ +import React, { + useContext, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import clsx from "clsx"; +import Link from "./Link"; +import { H3 } from "./Typography"; +import { partnerLogos, type PartnerLogo } from "../data/partnerLogos"; +import { componentUsage } from "../data/componentUsage"; +import { useComponentHighlight } from "../context/ComponentHighlightContext"; +import { floatingTooltipPosition } from "../utils/floatingTooltip"; +import { + hoverIntentHandlers, + isHoverIntent, + wantsFocusHint, +} from "../utils/hoverIntent"; +import { OVERTURE_DOCUMENTATION_FUNDING } from "../constants/externalLinks"; + +// The one line under the heading. Only shown where the interaction it describes +// is possible: tablet-up (see _logo-carousel.scss). +const HOVER_HINT = + "Hover a component above, or a platform below, to see how they connect"; + +// Pixels per millisecond. Slow enough that a logo is legible for a couple of +// seconds as it drifts past, not just a blur you have to stop to read. +const SPEED = 0.015; + +// How long the loop keeps its hands off `scrollLeft` after something other +// than itself last moved it. A touch swipe hands over to momentum once the +// finger lifts, and every momentum frame refreshes this, so the figure only +// has to outlast the gap between two frames of that glide rather than the +// whole of it. +const EXTERNAL_SCROLL_HOLD = 700; + +// Pixels of mouse movement past a mousedown before this treats the gesture +// as a drag rather than a click. Below this, nothing here touches +// `scrollLeft` or captures the pointer, so a plain click still reaches +// whatever logo link is under it — see the note on `handlePointerDown`. +const DRAG_THRESHOLD = 4; + +type TooltipInfo = { id: string; name: string; impact: string; rect: DOMRect }; + +// Internal to this file, not exported: a way for any LogoItem, however deep +// (the marquee's two lists, or the filtered view), to hand its impact +// tooltip up to the one place that renders it, without threading a callback +// prop through LogoList as well. +const LogoTooltipContext = React.createContext< + (info: TooltipInfo | null) => void +>(() => {}); + +function LogoItem({ + logo, + hiddenForLoop, + highlighted, +}: { + logo: PartnerLogo; + hiddenForLoop: boolean; + highlighted: boolean; +}) { + const { setHighlightedPlatform } = useComponentHighlight(); + const setTooltip = useContext(LogoTooltipContext); + const content = ( + {hiddenForLoop + ); + // No production date rendered under the logo (still in + // data/partnerLogos.ts, unused here). A logo with an impact statement + // skips the plain browser `title` in favor of the tooltip below, so the + // two never stack. + const title = logo.name; + const itemClassName = clsx( + "LogoCarousel__item", + highlighted && "LogoCarousel__item--highlighted", + ); + + // Pointer events, not onMouseEnter/onMouseLeave: @docusaurus/Link spreads + // `...props` and then unconditionally sets its own `onMouseEnter` after + // (for its hover-preload behaviour), silently discarding whatever the + // caller passed in. Pointer events are untouched by it. + const handleEnter = (element: HTMLElement) => { + setHighlightedPlatform(logo.id); + if (logo.impact) { + setTooltip({ + id: logo.id, + name: logo.name, + impact: logo.impact, + rect: element.getBoundingClientRect(), + }); + } + }; + const handleLeave = () => { + setHighlightedPlatform(null); + setTooltip(null); + }; + // Guarded to mouse-only pointer events and keyboard-caused focus: a + // touchscreen has no hover, which is why the hint line above the logos and + // the diagram this highlights are both `display: none` below tablet-up + // (_logo-carousel.scss, _home.scss). A tap on a logo follows its link, as + // it did before. + const highlightHandlers = hoverIntentHandlers( + (event) => handleEnter(event.currentTarget), + handleLeave, + ); + + if (!logo.href) { + return ( +
    • + + {content} + +
    • + ); + } + return ( +
    • + + {content} + +
    • + ); +} + +const LogoList = React.forwardRef( + function LogoList({ hidden }, ref) { + return ( +
        + {partnerLogos.map((logo) => ( + + ))} +
      + ); + }, +); + +/** + * Who runs Overture, scrolling right below the hero: a continuous marquee, + * so every logo shows at once rather than hiding behind an arrow. + * + * The list renders twice back to back inside a real scroll container + * (`overflow-x: auto`), not a CSS transform animation, so a + * `requestAnimationFrame` loop can advance `scrollLeft` each frame while + * still letting a trackpad swipe, shift+wheel, or mouse drag take over + * without fighting it. The second copy is `aria-hidden`/`tabIndex={-1}` (it + * only exists to make the loop seamless) and drops out entirely under + * `prefers-reduced-motion`. + * + * Two independent links through `ComponentHighlightContext`: hovering a + * component in HeroDiagram swaps the marquee for a static list of the + * platforms that use it (dimming in place would miss anything currently + * off-screen); hovering a logo here sets `highlightedPlatform`, which + * HeroDiagram reads to highlight its own matching components. + * + * The impact-statement tooltip is `position: fixed`, not the `position: + * absolute` popup HeroDiagram's tooltip uses, because `&__viewport` needs + * `overflow-y: hidden` for the marquee illusion, which would clip anything + * anchored inside it. + */ +export default function LogoCarousel() { + const viewportRef = useRef(null); + const listRef = useRef(null); + const stageRef = useRef(null); + const filteredListRef = useRef(null); + const pausedRef = useRef(false); + const draggingRef = useRef(false); + // A finger or pen resting on the viewport, separate from `draggingRef` + // (the mouse drag this component runs itself) — touch scrolling is the + // browser's; this only needs to stay out of its way. + const touchHoldRef = useRef(false); + const dragStartRef = useRef({ x: 0, scrollLeft: 0 }); + // A mousedown not yet proven a drag, kept separate from + // `dragStartRef`/`draggingRef` so a plain click never captures the + // pointer — see `DRAG_THRESHOLD`. + const dragCandidateRef = useRef<{ + pointerId: number; + x: number; + scrollLeft: number; + } | null>(null); + const highlightedComponentRef = useRef(null); + const { highlightedComponent } = useComponentHighlight(); + const [tooltip, setTooltip] = useState(null); + const [filteredScale, setFilteredScale] = useState(1); + + // The filtered view only ever shrinks to fit one line, never grows or + // wraps. `scrollWidth` reads the list's natural width regardless of any + // transform already applied, so recomputing repeatedly is safe. + useLayoutEffect(() => { + const list = filteredListRef.current; + const stage = stageRef.current; + if (!highlightedComponent || !list || !stage) { + setFilteredScale(1); + return; + } + + const recompute = () => { + const naturalWidth = list.scrollWidth; + if (naturalWidth === 0) return; + setFilteredScale(Math.min(1, stage.clientWidth / naturalWidth)); + }; + + recompute(); + const observer = new ResizeObserver(recompute); + observer.observe(stage); + return () => observer.disconnect(); + }, [highlightedComponent]); + + useEffect(() => { + highlightedComponentRef.current = highlightedComponent; + }, [highlightedComponent]); + + useEffect(() => { + const viewport = viewportRef.current; + const list = listRef.current; + if (!viewport || !list) return; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + return; + } + + let frame: number; + let last: number | null = null; + // `scrollLeft` is integer-quantized; at this speed each frame's move is + // under a pixel, so writing straight to it would round to zero forever. + // Tracked as a float here, only rounded on write. + let position = viewport.scrollLeft; + // The previous frame's `scrollLeft`, the only way to tell this loop's + // own writes apart from the visitor's. + let lastSeen: number | null = null; + // `-Infinity`, not `0`: a plain zero would read as a recent scroll for + // the first `EXTERNAL_SCROLL_HOLD` of the page's life. + let externalScrollAt = Number.NEGATIVE_INFINITY; + + const tick = (now: number) => { + const elapsed = last === null ? 0 : now - last; + last = now; + const listWidth = list.offsetWidth; + if (listWidth === 0) { + frame = requestAnimationFrame(tick); + return; + } + + // Native scrolling (touch, trackpad, shift+wheel) only shows up as + // `scrollLeft` moving since the last frame; nothing else marks it. + const actual = viewport.scrollLeft; + if (lastSeen !== null && Math.abs(actual - lastSeen) > 1) { + externalScrollAt = now; + } + + if ( + pausedRef.current || + draggingRef.current || + touchHoldRef.current || + now - externalScrollAt < EXTERNAL_SCROLL_HOLD || + highlightedComponentRef.current !== null + ) { + // Resync from the real offset so resuming continues from there, + // not a snap back. + position = actual; + } else { + position += SPEED * elapsed; + if (position >= listWidth) { + position -= listWidth; + } else if (position < 0) { + position += listWidth; + } + viewport.scrollLeft = position; + } + + // Read back after writing, not before: `scrollLeft` is quantized and + // clamped, so the next frame must compare against what's actually + // there, not what was requested. + lastSeen = viewport.scrollLeft; + frame = requestAnimationFrame(tick); + }; + + frame = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frame); + }, []); + + // Mouse hover only: a touchscreen has none, and the compatibility + // `mouseenter` a tap fires has no matching `mouseleave`, which would leave + // the marquee paused for good after one tap. + const pause = (event: React.PointerEvent) => { + if (!isHoverIntent(event)) return; + pausedRef.current = true; + }; + const resume = (event: React.PointerEvent) => { + if (!isHoverIntent(event)) return; + pausedRef.current = false; + }; + // `event.target`, not `currentTarget`: React's `onFocus` is `focusin`, so + // `currentTarget` is the viewport, but it's the focused link inside it + // that needs checking. Blur stays unconditional; clearing an unset pause + // costs nothing. + const pauseForFocus = (event: React.FocusEvent) => { + if (!wantsFocusHint(event.target as HTMLElement)) return; + pausedRef.current = true; + }; + const resumeFromFocus = () => { + pausedRef.current = false; + }; + + const handlePointerDown = (event: React.PointerEvent) => { + // Touch and pen already get native drag-to-scroll; only mouse drag + // needs handling here. The loop still needs to know contact exists — a + // still finger scrolls nothing for it to notice otherwise. + if (event.pointerType !== "mouse") { + touchHoldRef.current = true; + return; + } + const viewport = viewportRef.current; + if (!viewport) return; + // Not yet a drag: `draggingRef` stays false and the pointer uncaptured + // until `handlePointerMove` confirms movement past `DRAG_THRESHOLD`. + // Capturing here instead would retarget the browser's own `click` onto + // this viewport, making every logo unclickable by mouse. + dragCandidateRef.current = { + pointerId: event.pointerId, + x: event.clientX, + scrollLeft: viewport.scrollLeft, + }; + }; + + const handlePointerMove = (event: React.PointerEvent) => { + const viewport = viewportRef.current; + if (!viewport) return; + if (draggingRef.current) { + const { x, scrollLeft } = dragStartRef.current; + viewport.scrollLeft = scrollLeft - (event.clientX - x); + return; + } + const candidate = dragCandidateRef.current; + if (!candidate || candidate.pointerId !== event.pointerId) return; + if (Math.abs(event.clientX - candidate.x) < DRAG_THRESHOLD) return; + // Crossed the slop: a drag, not a click. Captured only now, so the + // click-retargeting only applies to a confirmed drag. + draggingRef.current = true; + viewport.setPointerCapture(event.pointerId); + dragStartRef.current = { x: candidate.x, scrollLeft: candidate.scrollLeft }; + viewport.scrollLeft = candidate.scrollLeft - (event.clientX - candidate.x); + }; + + // Before the `draggingRef` guard: touch never sets that ref, so returning + // early here would leave the hold set for good after the first tap. + const endDrag = (event: React.PointerEvent) => { + touchHoldRef.current = false; + dragCandidateRef.current = null; + if (!draggingRef.current) return; + draggingRef.current = false; + viewportRef.current?.releasePointerCapture(event.pointerId); + }; + + const matches = highlightedComponent + ? partnerLogos.filter((logo) => + componentUsage[logo.id]?.includes(highlightedComponent), + ) + : null; + + return ( + +
      +
      +
      +
      +

      {HOVER_HINT}

      +
      + +
      +
      +
      +
      +
      +
      +
      + + {/* Always mounted, not conditional on `matches`, so the crossfade + with the marquee above has something to transition between — + a freshly-mounted element has no "before" state to animate + from. */} +
      + {matches && + (matches.length > 0 ? ( +
        + {matches.map((logo) => ( + + ))} +
      + ) : ( +

      + No confirmed platforms yet for this component. +

      + ))} +
      +
      +
      +
      +
      + + {tooltip && ( + + )} +
      + ); +} diff --git a/website/src/marketing/components/MarketingFooter.tsx b/website/src/marketing/components/MarketingFooter.tsx new file mode 100644 index 00000000..a5cd2e97 --- /dev/null +++ b/website/src/marketing/components/MarketingFooter.tsx @@ -0,0 +1,168 @@ +import React from "react"; +import Link from "./Link"; +import { + ABOUT_BAND, + COLLABORATE_PATH, + IMPACT_PATH, + PRIVACY_PATH, + PRODUCTS_PATH, + TERMS_PATH, +} from "../constants/pages"; +import { + DOCUMENTATION_LINK, + DOCS_COMMUNITY, + EMAIL_LINK, + NETLIFY_LINK, + NETLIFY_IMAGE_LINK, + OICR_LINK, + OVERTURE_DOCUMENTATION_CITING, + OVERTURE_DOCUMENTATION_FUNDING, + OVERTURE_DOCUMENTATION_LICENSING, + OVERTURE_DOCUMENTATION_TEAM_LINK, + OVERTURE_GITHUB_LINK, + OVERTURE_SUPPORT, +} from "../constants/externalLinks"; + +const OICR_LOGO = "/img/marketing/chrome/oicr_logo.svg"; + +type FooterLink = { name: string; url: string; external?: boolean }; +type FooterColumn = { heading: string; links: FooterLink[] }; + +/** + * Four columns, replacing a flat list of eight links with no way to group + * them as pages were added. The columns are the difference between a + * footer and a pile of links. + * + * The only navigation left to the funding/citation content (Impact column, + * leaving the site) and to `ABOUT_BAND`, since the navbar's About item is + * gone. + * + * `external` drives the arrow, not a URL test: `Link` already decides how to + * open a link, and marking these by hand keeps the two decisions independent. + */ +const columns: FooterColumn[] = [ + { + heading: "Platform", + links: [ + { name: "Products", url: PRODUCTS_PATH }, + { name: "Documentation", url: DOCUMENTATION_LINK, external: true }, + { name: "GitHub", url: OVERTURE_GITHUB_LINK, external: true }, + { + name: "Software licensing", + url: OVERTURE_DOCUMENTATION_LICENSING, + external: true, + }, + ], + }, + { + // Two of these three leave the site: /impact/publications/ and + // /about/funding/ are gone, their maintained content on the docs site + // instead. "How to cite us" is a clearer label than "Publications" for + // a link a reader follows to get a citation string. + heading: "Impact", + links: [ + { name: "Deployments", url: IMPACT_PATH }, + { + name: "How to cite us", + url: OVERTURE_DOCUMENTATION_CITING, + external: true, + }, + { name: "Funding", url: OVERTURE_DOCUMENTATION_FUNDING, external: true }, + ], + }, + { + heading: "Connect", + links: [ + { name: "Collaboration", url: COLLABORATE_PATH }, + { name: "Support forum", url: OVERTURE_SUPPORT, external: true }, + { name: "Community", url: DOCS_COMMUNITY, external: true }, + { name: "Contact", url: EMAIL_LINK }, + ], + }, + { + heading: "About", + links: [ + // /about-us/ is gone; the "What we do" band on the home page carries + // what it said, via `ABOUT_BAND` in constants/pages.ts. + { name: "Our story", url: ABOUT_BAND }, + { name: "Team", url: OVERTURE_DOCUMENTATION_TEAM_LINK, external: true }, + { name: "OICR", url: OICR_LINK, external: true }, + ], + }, +]; + +const NetlifyBadge = ({ className }: { className: string }) => ( +
      + + Deploys by Netlify + +
      +); + +/** + * The marketing site's own footer, paired with MarketingNavbar. + * + * Wrapped in `.marketing` for the same reason the navbar is: these render + * outside the page wrapper that carries that class, and without it the styles + * scoped under it would not reach them. + */ +export default function MarketingFooter() { + return ( +
      +
      +
      + + Ontario Institute for Cancer Research + + + + + +
      +
      +
      +
      +
      © {new Date().getFullYear()} Overture.
      + +
      + + {/* Acknowledgements used to repeat here, a second link to a page + already in the row above. Retired with the page itself. */} +
      + Privacy + | + Terms & Conditions +
      + +
      +
      +
      +
      + ); +} diff --git a/website/src/marketing/components/MarketingNavbar.tsx b/website/src/marketing/components/MarketingNavbar.tsx new file mode 100644 index 00000000..74179257 --- /dev/null +++ b/website/src/marketing/components/MarketingNavbar.tsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import { useLocation } from "@docusaurus/router"; +import SearchBar from "@theme/SearchBar"; +import Link from "./Link"; +import { + HOME_PATH, + IMPACT_PATH, + PRODUCTS_PATH, + COLLABORATE_PATH, +} from "../constants/pages"; +import { + DOCUMENTATION_LINK, + OVERTURE_GITHUB_LINK, +} from "../constants/externalLinks"; + +const LOGO = "/img/marketing/chrome/overture_logo.svg"; + +// Three items plus two actions. `Documentation` moved into the actions +// group since it leaves the site; `Support` dropped out entirely and became +// "Support forum" in the footer's Connect column. +// +// No `About`: it was the one item that scrolled the page a visitor was +// already on rather than navigating, since /about-us/ was removed. "Who +// are these people" is answered by that band and by the footer's Our +// story, both via `ABOUT_BAND` in constants/pages.ts. Don't re-add it +// without a real route to point at. +// +// Every label left here points at the page it names. +const navLinks = [ + { name: "Products", url: PRODUCTS_PATH }, + { name: "Impact", url: IMPACT_PATH }, + { name: "Collaboration", url: COLLABORATE_PATH }, +]; + +// Both leave the site, so both carry the external-link icon. No `Get +// Started` beside them: it read as a second call to action, and the docs +// quickstart it pointed at is already one click away via `Docs`. +const navActions = [ + { name: "Docs", url: DOCUMENTATION_LINK }, + { name: "GitHub", url: OVERTURE_GITHUB_LINK }, +]; + +/** + * The marketing site's own navigation. + * + * The documentation navbar (Develop, Deploy, Use, Community) is the wrong + * navigation for these pages, and showing it made the two sites look like one. + * The megamenu this could have carried is already unreachable elsewhere on + * the site, and the mobile menu needs nothing more than a single piece of + * state to open and close. + * + * Docusaurus renders the navbar outside the content wrapper that carries + * `.marketing`, so this brings its own — as a wrapper, not a second class on + * `