feat: create component library + use in relevant places (#2229)

This commit is contained in:
Elijah Potter 2025-11-24 09:45:05 -07:00 committed by GitHub
parent da9e2ba0d6
commit 1377ab51a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 2163 additions and 823 deletions

View file

@ -27,7 +27,11 @@ WORKDIR /usr/build/
COPY . .
COPY --from=wasm-build /usr/build/harper-wasm/pkg /usr/build/harper-wasm/pkg
RUN pnpm install --shamefully-hoist
RUN pnpm install --engine-strict=false --shamefully-hoist
WORKDIR /usr/build/packages/components
RUN pnpm install --engine-strict=false --shamefully-hoist
RUN pnpm build
WORKDIR /usr/build/packages/harper.js
@ -37,7 +41,7 @@ WORKDIR /usr/build/packages/lint-framework
RUN pnpm build
WORKDIR /usr/build/packages/web
RUN pnpm install --shamefully-hoist
RUN pnpm install --engine-strict=false --shamefully-hoist
RUN pnpm build
FROM node:${NODE_VERSION}

View file

@ -3,6 +3,15 @@ format:
cargo fmt
pnpm format
# Build the shared component library
build-components:
#!/usr/bin/env bash
set -eo pipefail
cd "{{justfile_directory()}}/packages/components"
pnpm install --engine-strict=false
pnpm build
# Build the WebAssembly module
build-wasm:
#!/usr/bin/env bash
@ -80,7 +89,7 @@ build-wp: build-harperjs
pnpm plugin-zip
# Compile the website's dependencies and start a development server. Note that if you make changes to `harper-wasm`, you will have to re-run this command.
dev-web: build-harperjs build-lint-framework
dev-web: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail
@ -89,7 +98,7 @@ dev-web: build-harperjs build-lint-framework
pnpm dev
# Build the Harper website.
build-web: build-harperjs build-lint-framework
build-web: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail
@ -110,7 +119,7 @@ build-obsidian: build-harperjs
zip harper-obsidian-plugin.zip manifest.json main.js
# Build the Chrome extension.
build-chrome-plugin: build-harperjs build-lint-framework
build-chrome-plugin: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail
@ -120,7 +129,7 @@ build-chrome-plugin: build-harperjs build-lint-framework
pnpm zip-for-chrome
# Start a development server for the Chrome extension.
dev-chrome-plugin: build-harperjs build-lint-framework
dev-chrome-plugin: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail
@ -130,7 +139,7 @@ dev-chrome-plugin: build-harperjs build-lint-framework
pnpm dev
# Build the Firefox extension.
build-firefox-plugin: build-harperjs build-lint-framework
build-firefox-plugin: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail
@ -272,7 +281,7 @@ check-rust: auditdictionary
# Perform format and type checking.
check: check-rust check-js build-web
check-js: build-harperjs build-lint-framework
check-js: build-harperjs build-lint-framework build-components
#!/usr/bin/env bash
set -eo pipefail

View file

@ -1,36 +1,88 @@
@import "tailwindcss";
@plugin "flowbite/plugin";
@import "components/components.css";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary-50: #fefee3;
--color-primary-100: #e3eccf;
--color-primary-200: #c9dabc;
--color-primary-300: #afc8a9;
--color-primary-400: #95b696;
--color-primary-500: #7aa482;
--color-primary-600: #60926f;
--color-primary-700: #46805c;
--color-primary-800: #2c6e49;
--color-primary-900: #23583a;
--font-sans: "Atkinson Hyperlegible", sans-serif;
--font-serif: Domine, serif;
--color-accent-peach: #ffc9b9;
--color-accent-sand: #d68c45;
--color-primary-50: #fef4e7; /* honey bronze */
--color-primary-100: #fce9cf;
--color-primary-200: #f9d49f;
--color-primary-300: #f7be6e;
--color-primary-400: #f4a83e;
--color-primary: #f1920e;
--color-primary-600: #c1750b;
--color-primary-700: #915808;
--color-primary-800: #603b06;
--color-primary-900: #301d03;
--color-primary-950: #221402;
--color-accent-50: #fee7e9; /* hot fuchsia */
--color-accent-100: #fccfd3;
--color-accent-200: #f99fa6;
--color-accent-300: #f76e7a;
--color-accent-400: #f43e4d;
--color-accent: #f10e21;
--color-accent-600: #c10b1a;
--color-accent-700: #910814;
--color-accent-800: #60060d;
--color-accent-900: #300307;
--color-accent-950: #220205;
--color-cream: #fef4e7; /* simple cream */
--color-cream-100: #fce9cf;
--color-cream-200: #f9d49f;
--color-cream-300: #f7be6e;
--color-cream-400: #f4a83e;
--color-cream-500: #f1920e;
--color-cream-600: #c1750b;
--color-cream-700: #915808;
--color-cream-800: #603b06;
--color-cream-900: #301d03;
--color-cream-950: #221402;
--color-champagne-mist-50: #fef4e7;
--color-champagne-mist-100: #fce9cf;
--color-champagne-mist-200: #fad49e;
--color-champagne-mist-300: #f7be6e;
--color-champagne-mist-400: #f5a83d;
--color-champagne-mist-500: #f2930d;
--color-champagne-mist-600: #c2750a;
--color-champagne-mist-700: #915808;
--color-champagne-mist-800: #613b05;
--color-champagne-mist-900: #301d03;
--color-champagne-mist-950: #221502;
--color-white: #fffdfa;
--color-white-100: #fceacf;
--color-white-200: #fad59e;
--color-white-300: #f7c06e;
--color-white-400: #f5ab3d;
--color-white-500: #f2960d;
--color-white-600: #c2780a;
--color-white-700: #915a08;
--color-white-800: #613c05;
--color-white-900: #301e03;
--color-white-950: #221502;
}
@source "./node_modules/flowbite-svelte/dist";
code {
@apply bg-primary-100 rounded p-1;
@apply bg-primary-100 rounded p-1 dark:text-black;
}
body {
@apply min-h-screen bg-white text-gray-900 transition-colors duration-150;
#app {
@apply min-h-screen bg-white text-black dark:bg-black dark:text-white transition-colors duration-150;
font-family:
Atkinson Hyperlegible,
sans-serif;
}
.dark body,
body.dark {
@apply bg-slate-950 text-slate-100;
h1,
h2,
h3,
h4 {
font-family: Domine, serif;
}

View file

@ -6,6 +6,18 @@
<link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Harper Settings</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Domine:wght@400..700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet"
/>
</head>
<body>

View file

@ -32,7 +32,6 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "catalog:",
"flowbite": "^3.1.2",
"flowbite-svelte": "^0.44.18",
"gulp": "^5.0.0",
"gulp-zip": "^6.0.0",
"http-server": "^14.1.1",
@ -51,6 +50,7 @@
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@webcomponents/custom-elements": "^1.6.0",
"components": "workspace:*",
"harper.js": "workspace:*",
"lint-framework": "workspace:*",
"lodash-es": "^4.17.21",

View file

@ -5,6 +5,18 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Harper: The Private Grammar Checker</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Domine:wght@400..700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet"
/>
</head>
<body>

View file

@ -15,12 +15,15 @@ export function makeExtensionCSP(isDev: boolean): string {
const scriptSrc = ["'self'", "'wasm-unsafe-eval'"]; // minimum, cannot add more
const objectSrc = ["'self'"]; // standard
const connectSrc = ["'self'"]; // WebSocket goes here
const styleSrc = ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'];
const fontSrc = ["'self'", 'https://fonts.gstatic.com', 'data:'];
if (isDev) {
// `ws://` and `http://` use the same host:port → list both
connectSrc.push('http://localhost:5173', 'ws://localhost:5173');
// include the 127.0.0.1 loopback in case you switch hosts
connectSrc.push('http://127.0.0.1:*', 'ws://127.0.0.1:*');
styleSrc.push('http://localhost:5173', 'http://127.0.0.1:*');
}
connectSrc.push('https://writewithharper.com');
@ -30,6 +33,8 @@ export function makeExtensionCSP(isDev: boolean): string {
`script-src ${scriptSrc.join(' ')}`,
`object-src ${objectSrc.join(' ')}`,
`connect-src ${connectSrc.join(' ')}`,
`style-src ${styleSrc.join(' ')}`,
`font-src ${fontSrc.join(' ')}`,
].join('; ')};`;
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Input, Select } from 'flowbite-svelte';
import { Button, Card, Input, Select, Textarea } from 'components';
import { Dialect, type LintConfig } from 'harper.js';
import logo from '/logo.png';
import ProtocolClient from '../ProtocolClient';
@ -144,29 +144,31 @@ async function exportEnabledDomainsCSV() {
console.error('Failed to export enabled domains JSON:', e);
}
}
// Import removed
</script>
<!-- centered wrapper with side gutters -->
<div class="mx-auto max-w-screen-md px-4 text-gray-900 dark:text-slate-100">
<header class="flex items-center gap-2 px-3 py-2 bg-gray-50/60 border-b border-gray-200 rounded-t-lg text-gray-900 dark:bg-slate-900/70 dark:border-slate-800 dark:text-slate-100">
<img src={logo} alt="Harper logo" class="h-6 w-auto rounded-lg" />
<span class="font-semibold text-sm">Harper</span>
</header>
<div class="min-h-screen px-4 py-10">
<div class="mx-auto max-w-screen-lg space-y-4">
<Card class="flex items-center gap-3">
<div class="flex h-9 w-9 items-center justify-center rounded-xl">
<img src={logo} alt="Harper logo" class="h-5 w-auto" />
</div>
<div class="flex flex-col">
<h1 class="text-base tracking-wide font-serif">Harper</h1>
<p class="text-xs">Chrome Extension Settings</p>
</div>
</Card>
<main class="p-6 space-y-10 text-sm border border-gray-200 rounded-b-lg shadow-sm bg-white dark:bg-slate-900 dark:border-slate-800 dark:text-slate-100">
<!-- ── GENERAL ───────────────────────────── -->
<section class="space-y-6">
<h3 class="pb-1 border-b border-gray-200 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-slate-400">General</h3>
<Card class="space-y-6">
<h2 class="pb-1 text-xs uppercase tracking-wider">General</h2>
<div class="space-y-5">
<div class="flex items-center justify-between">
<span class="font-medium">English Dialect</span>
<h3 class="text-sm">English Dialect</h3>
<Select
size="sm"
color="primary"
class="w-44 dark:bg-slate-900 dark:text-slate-100 dark:border-slate-700"
class="w-44"
bind:value={dialect}
>
<option value={Dialect.American}>🇺🇸 American</option>
@ -180,35 +182,34 @@ async function exportEnabledDomainsCSV() {
<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-medium">Enable on New Sites by Default</span>
<span class="font-light text-gray-500 dark:text-slate-400">Can make some apps behave abnormally.</span>
<h3 class="text-sm">Enable on New Sites by Default</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">Can make some apps behave abnormally.</p>
</div>
<input type="checkbox" bind:checked={defaultEnabled}/>
<input type="checkbox" bind:checked={defaultEnabled} class="h-5 w-5" />
</div>
</div>
<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-medium">Export Enabled Domains</span>
<span class="font-light text-gray-500 dark:text-slate-400">Downloads JSON of domains explicitly enabled.</span>
<h3 class="text-sm">Export Enabled Domains</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">Downloads JSON of domains explicitly enabled.</p>
</div>
<Button size="sm" color="light" on:click={exportEnabledDomainsCSV}>Export JSON</Button>
<Button size="sm" on:click={exportEnabledDomainsCSV}>Export JSON</Button>
</div>
</div>
<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-medium">Activation Key</span>
<span class="font-light text-gray-500 dark:text-slate-400">If you're finding that you're accidentally triggering Harper.</span>
<h3 class="text-sm">Activation Key</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">
If you're finding that you're accidentally triggering Harper.
</p>
</div>
<Select
size="sm"
color="primary"
class="w-44 dark:bg-slate-900 dark:text-slate-100 dark:border-slate-700"
class="w-44"
bind:value={activationKey}
>
<option value={ActivationKey.Shift}>Double Shift</option>
@ -221,32 +222,31 @@ async function exportEnabledDomainsCSV() {
<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-medium">User Dictionary</span>
<span class="font-light text-gray-500 dark:text-slate-400">Each word should be on its own line.</span>
<h3 class="text-sm">User Dictionary</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">Each word should be on its own line.</p>
</div>
<textarea
<Textarea
bind:value={userDict}
class="ml-4 min-h-[6rem] w-64 rounded border border-gray-300 bg-white p-2 text-sm text-gray-900 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
></textarea>
></Textarea>
</div>
</div>
</section>
</Card>
<!-- ── RULES ─────────────────────────────── -->
<section class="space-y-4">
<Card class="space-y-4">
<div class="flex items-center justify-between gap-4">
<h3 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-slate-400">Rules</h3>
<h2 class="text-xs uppercase tracking-wider">Rules</h2>
<Input
bind:value={searchQuery}
placeholder="Search for a rule…"
size="sm"
class="w-60 dark:bg-slate-900 dark:text-slate-100 dark:border-slate-700"
class="w-60"
/>
</div>
<div class="flex flex-wrap gap-3">
<Button size="sm" color="light" on:click={resetRulesToDefaults}>Reset to Default Rules</Button>
<Button size="sm" color="light" on:click={toggleAllRules}>
<Button size="sm" on:click={resetRulesToDefaults}>Reset to Default Rules</Button>
<Button size="sm" on:click={toggleAllRules}>
{anyRulesEnabled ? 'Disable All Rules' : 'Enable All Rules'}
</Button>
</div>
@ -256,21 +256,19 @@ async function exportEnabledDomainsCSV() {
(lintDescriptions[key] ?? '').toLowerCase().includes(searchQueryLower) ||
key.toLowerCase().includes(searchQueryLower)
) as [key, value]}
<div class="space-y-4 max-h-80 overflow-y-auto pr-1">
<div class="rule-scroll space-y-4 max-h-80 overflow-y-auto pr-1">
<!-- rule card sample -->
<div class="rounded-lg border border-gray-200 p-3 shadow-sm bg-white dark:border-slate-700 dark:bg-slate-900">
<div class="flex items-start justify-between gap-4">
<div class="space-y-0.5">
<p class="font-medium text-gray-900 dark:text-slate-100">{key}</p>
<p class="text-xs text-gray-600 dark:text-slate-400 dark:[&_code]:text-black">{@html lintDescriptions[key]}</p>
<h3 class="text-sm">{key}</h3>
<p class="text-xs">{@html lintDescriptions[key]}</p>
</div>
<Select
size="sm"
size="md"
value={configValueToString(value)}
on:change={(e) => {
lintConfig[key] = configStringToValue(e.target.value);
}}
class="max-w-[10rem] dark:bg-slate-900 dark:text-slate-100 dark:border-slate-700"
>
<option value="default"> Default</option>
<option value="enable"> On</option>
@ -278,9 +276,8 @@ async function exportEnabledDomainsCSV() {
</Select>
</div>
</div>
</div>
{/each}
</section>
</main>
</Card>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Select, Toggle } from 'flowbite-svelte';
import { Button, Select, Toggle } from 'components';
import ProtocolClient from '../ProtocolClient';
let enabled = $state(true);
@ -45,19 +45,20 @@ function toggleDomainEnabled() {
<section class="flex flex-row items-center gap-3 py-6">
<Button
size="lg"
class="rounded-full aspect-square h-16 w-16 p-0 shadow-md transition-colors flex flex-row justify-center"
style="background-color: {enabled ? 'var(--color-primary-500)' : '#d1d5db'};"
class="rounded-full! aspect-square h-16 w-16 p-0 shadow-lg transition-colors flex! flex-row justify-center"
color={enabled ? 'var(--color-primary)' : 'var(--color-cream-50)'}
on:click={toggleDomainEnabled}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-9 w-9 text-white"
class="h-9 w-9"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
color={enabled ? 'var(--color-cream-50)' : 'var(--color-primary)'}
stroke-linecap="round"
stroke-linejoin="round"
d="M12 5v7m5.657-4.657a8 8 0 11-11.314 0"

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Badge, Button } from 'flowbite-svelte';
import { Badge, Button } from 'components';
let { onConfirm }: { onConfirm: () => void } = $props();
@ -26,7 +26,7 @@ const steps = [
{/each}
</ul>
<Button color="primary" outline class="w-full h-10" on:click={onConfirm}>
<Button color="primary" class="w-full h-10" on:click={onConfirm}>
Let's start writing
</Button>
</main>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
import { Button } from 'flowbite-svelte';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { Button, Link } from 'components';
import Fa from 'svelte-fa';
import logo from '/logo.png';
import { main, type PopupState } from '../PopupState';
@ -26,16 +26,16 @@ function openSettings() {
</script>
<div class="w-[340px] border border-gray-200 bg-white font-sans flex flex-col rounded-lg shadow-sm select-none dark:bg-slate-900 dark:border-slate-800 dark:text-slate-100">
<header class="flex flex-row justify-between items-center gap-2 px-3 py-2 rounded-t-lg bg-gray-50/60 text-gray-900 dark:bg-slate-900/70 dark:text-slate-100">
<header class="flex flex-row justify-between items-center gap-2 px-3 py-2 rounded-t-lg">
<div class="flex flex-row justify-start items-center">
<img src={logo} alt="Harper logo" class="h-6 w-auto rounded-lg mx-2" />
<span class="font-semibold text-sm">Harper</span>
</div>
{#if popupState.page != "main"}
<Button outline on:click={() => {
<Button on:click={() => {
popupState = main();
}}><Fa icon={faCaretLeft}/></Button>
}}><Fa icon={faArrowLeft}/></Button>
{/if}
</header>
@ -48,9 +48,9 @@ function openSettings() {
{/if}
<footer class="flex items-center justify-center gap-6 px-3 py-2 text-sm border-t border-gray-100 rounded-b-lg bg-white/60 dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-100">
<a href="https://github.com/Automattic/harper" target="_blank" rel="noopener" class="text-primary-600 hover:underline">GitHub</a>
<a href="https://discord.com/invite/JBqcAaKrzQ" target="_blank" rel="noopener" class="text-primary-600 hover:underline">Discord</a>
<a href="https://writewithharper.com" target="_blank" rel="noopener" class="text-primary-600 hover:underline">Discover</a>
<button class="text-primary-600 hover:underline" onclick={openSettings}>Settings</button>
<Link href="https://github.com/Automattic/harper" target="_blank" rel="noopener" class="text-primary">GitHub</Link>
<Link href="https://discord.com/invite/JBqcAaKrzQ" target="_blank" rel="noopener" class="text-primary">Discord</Link>
<Link href="https://writewithharper.com" target="_blank" rel="noopener" class="text-primary">Discover</Link>
<Link on:click={openSettings}>Settings</Link>
</footer>
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Checkbox, Input, Label } from 'flowbite-svelte';
import { Button, Checkbox, Input, Label } from 'components';
import ProtocolClient from '../ProtocolClient';
let {

24
packages/components/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
/dist
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -0,0 +1,58 @@
{
"name": "components",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run prepack",
"build:css": "TAILWIND_MODE=build tailwindcss -i ./src/lib/styles.css -o ./dist/components.css --minify",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"prepack": "svelte-kit sync && svelte-package && pnpm run build:css && publint",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"style": "./dist/components.css",
"sideEffects": [
"**/*.css"
],
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./components.css": "./dist/components.css",
"./style.css": "./dist/components.css"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"dependencies": {
"flowbite-svelte": "^0.44.24"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.47.1",
"@sveltejs/package": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/cli": "^4.1.16",
"@tailwindcss/vite": "^4.1.14",
"flowbite": "^3.1.2",
"publint": "^0.3.14",
"svelte": "^5.41.0",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3",
"vite": "^7.1.10"
},
"keywords": [
"svelte"
]
}

13
packages/components/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,101 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'svelte/elements';
import Link from './Link.svelte';
type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
type ButtonColor = 'primary' | 'light' | 'gray' | 'white' | 'dark';
export let size: ButtonSize = 'md';
export let color: ButtonColor | string = 'primary';
export let textColor: string | undefined = undefined;
export let pill = false;
export let href: AnchorHTMLAttributes['href'] = undefined;
export let target: AnchorHTMLAttributes['target'] = undefined;
export let rel: AnchorHTMLAttributes['rel'] = undefined;
export let type: ButtonHTMLAttributes['type'] = 'button';
export let disabled: boolean | undefined = undefined;
// Alias for the `class` attribute since `class` is a reserved TS keyword
export let className: string | undefined = undefined;
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const dispatch = createEventDispatcher();
const sizeClasses: Record<ButtonSize, string> = {
xs: 'px-3 py-2 text-xs',
sm: 'px-3 py-2 text-sm',
md: 'px-4 py-2.5 text-sm',
lg: 'px-5 py-3 text-base',
};
const colorClasses: Record<ButtonColor, string> = {
primary:
'text-white bg-primary-600 hover:bg-primary-700 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-700',
light:
'text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700',
gray: 'text-white bg-gray-800 hover:bg-gray-900 focus:ring-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:focus:ring-gray-900',
white:
'text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700',
dark: 'text-white bg-gray-900 hover:bg-black focus:ring-gray-300 dark:bg-gray-800 dark:hover:bg-black dark:focus:ring-gray-900',
};
const baseClasses =
'cursor-pointer inline-flex items-center gap-2 justify-center font-medium text-center transition-colors focus:outline-none focus:ring-4 disabled:opacity-50 disabled:cursor-not-allowed';
$: toneClass = colorClasses[color as ButtonColor] ?? colorClasses.primary;
$: shapeClass = pill ? 'rounded-full' : 'rounded-lg';
$: sizeClass = sizeClasses[size] ?? sizeClasses.md;
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes = [baseClasses, shapeClass, sizeClass, toneClass, restClass, className]
.filter(Boolean)
.join(' ');
$: colorOverride = colorClasses[color as ButtonColor] == null ? color : undefined;
$: inlineStyle =
colorOverride || textColor
? [
colorOverride ? `background-color: ${colorOverride} !important;` : null,
textColor ? `color: ${textColor} !important;` : null,
]
.filter(Boolean)
.join(' ')
: undefined;
function handleClick(event: MouseEvent) {
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
dispatch('click', event);
}
</script>
{#if href}
<Link
class={classes}
style={inlineStyle}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
rel={rel}
target={target}
on:click={handleClick}
{...restProps}
>
<slot />
</Link>
{:else}
<button
class={classes}
type={type}
{disabled}
{...restProps}
style={inlineStyle}
on:click={handleClick}
>
<slot />
</button>
{/if}

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let className: string | undefined = undefined;
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const baseClasses =
'rounded-lg px-4 py-3 shadow-lg backdrop-blur border border-cream-100 dark:border-cream-700';
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes = [baseClasses, restClass, className].filter(Boolean).join(' ');
</script>
<div class={classes} {...restProps}>
<slot />
</div>

View file

@ -0,0 +1,17 @@
<script lang="ts">
export let title: string;
export let open = false;
export let className = '';
const baseClasses =
'group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900';
$: detailsClass = `${baseClasses} ${className}`.trim();
</script>
<details class={detailsClass} {open}>
<summary class="cursor-pointer font-semibold marker:text-neutral-400">{title}</summary>
<div class="mt-3">
<slot />
</div>
</details>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { InputHTMLAttributes } from 'svelte/elements';
type InputSize = 'sm' | 'md' | 'lg';
export let type: InputHTMLAttributes['type'] = 'text';
export let value: InputHTMLAttributes['value'] = undefined;
export let placeholder: InputHTMLAttributes['placeholder'] = undefined;
export let className: string | undefined = undefined;
export let size: InputSize = 'md';
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const baseClasses =
'rounded-lg border border-cream-200 bg-white text-gray-900 placeholder-gray-500 shadow-sm outline-none transition focus:ring-2 focus:ring-primary-300 focus:border-cream-300 dark:border-cream-700 dark:bg-cream-900 dark:text-white dark:placeholder-cream-200 dark:focus:border-cream-600 dark:focus:ring-primary-600';
const sizeClasses: Record<InputSize, string> = {
sm: 'px-3 py-2 text-sm',
md: 'px-3 py-2.5 text-sm',
lg: 'px-4 py-3 text-base',
};
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes = [baseClasses, sizeClasses[size] ?? sizeClasses.md, restClass, className]
.filter(Boolean)
.join(' ');
</script>
<input
class={classes}
type={type}
placeholder={placeholder}
bind:value
{...restProps}
/>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { AnchorHTMLAttributes } from 'svelte/elements';
export let href: AnchorHTMLAttributes['href'] = undefined;
export let target: AnchorHTMLAttributes['target'] = undefined;
export let rel: AnchorHTMLAttributes['rel'] = undefined;
// Alias for the `class` attribute since `class` is a reserved TS keyword
export let className: string | undefined = undefined;
export let underline = false;
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const dispatch = createEventDispatcher();
function handleClick(event: MouseEvent) {
dispatch('click', event);
}
$: baseClasses = 'hover:underline text-primary dark:text-white';
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes =
[baseClasses, restClass, className, underline ? 'underline' : undefined]
.filter(Boolean)
.join(' ') || undefined;
$: resolvedRel = target === '_blank' && !rel ? 'noreferrer noopener' : rel;
</script>
<a href={href} target={target} rel={resolvedRel} class={classes} {...restProps}
on:click={handleClick}
>
<slot />
</a>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import type { SelectHTMLAttributes } from 'svelte/elements';
type SelectSize = 'sm' | 'md' | 'lg';
type SelectItem = {
value: SelectHTMLAttributes['value'];
name?: string;
label?: string;
disabled?: boolean;
selected?: boolean;
};
export let size: SelectSize = 'md';
export let items: SelectItem[] | undefined = undefined;
export let className: string | undefined = undefined;
export let value: SelectHTMLAttributes['value'] = undefined;
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const baseClasses =
'rounded-lg border border-cream-200 bg-white shadow-sm outline-none transition focus:ring-2 focus:ring-primary-300 focus:border-cream-300 dark:border-cream-700 dark:bg-cream-900 dark:text-white dark:focus:border-cream-600 dark:focus:ring-primary-600 text-left';
const sizeClasses: Record<SelectSize, string> = {
sm: 'pl-3 pr-8 py-2 text-sm',
md: 'pl-3 pr-8 py-2.5 text-sm',
lg: 'pl-4 pr-8 py-3 text-base',
};
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes = [baseClasses, sizeClasses[size] ?? sizeClasses.md, restClass, className]
.filter(Boolean)
.join(' ');
</script>
<select class={classes} bind:value {...restProps}>
{#if items?.length}
{#each items as item (item.value)}
<option value={item.value} disabled={item.disabled} selected={item.selected}>
{item.name ?? item.label ?? item.value}
</option>
{/each}
{:else}
<slot />
{/if}
</select>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import type { TextareaHTMLAttributes } from 'svelte/elements';
export let className: string | undefined = undefined;
export let value: TextareaHTMLAttributes['value'] = undefined;
export let rows: TextareaHTMLAttributes['rows'] = undefined;
export let cols: TextareaHTMLAttributes['cols'] = undefined;
let restClass: string | undefined;
let restProps: Record<string, unknown> = {};
const baseClasses =
'rounded-lg border border-cream-200 bg-white text-gray-900 shadow-sm placeholder-gray-500 outline-none transition focus:ring-2 focus:ring-primary-300 focus:border-cream-300 dark:border-cream-700 dark:bg-cream-900 dark:text-white dark:placeholder-cream-200 dark:focus:border-cream-600 dark:focus:ring-primary-600';
// Align with Svelte's `class` handling while allowing `className` as an alias.
$: ({ class: restClass, ...restProps } = $$restProps);
$: classes = [baseClasses, restClass, className].filter(Boolean).join(' ');
</script>
<textarea class={classes} bind:value rows={rows} cols={cols} {...restProps}>
<slot />
</textarea>

View file

@ -0,0 +1,23 @@
export {
Badge,
Checkbox,
Fileupload,
Label,
Radio,
Spinner,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
Toggle,
} from 'flowbite-svelte';
export { default as Button } from './Button.svelte';
export { default as Card } from './Card.svelte';
export { default as Collapsible } from './Collapsible.svelte';
export { default as Input } from './Input.svelte';
export { default as Link } from './Link.svelte';
export { default as Select } from './Select.svelte';
export { default as Textarea } from './Textarea.svelte';

View file

@ -0,0 +1,74 @@
@import "tailwindcss";
@plugin "flowbite/plugin";
@custom-variant dark (&:where(.dark, .dark *));
@source "./src/lib/**/*.{svelte,ts}";
@source "./node_modules/flowbite-svelte/**/*.{svelte,ts,js}";
@theme {
--color-primary-50: #fef4e7; /* honey bronze */
--color-primary-100: #fce9cf;
--color-primary-200: #f9d49f;
--color-primary-300: #f7be6e;
--color-primary-400: #f4a83e;
--color-primary: #f1920e;
--color-primary-600: #c1750b;
--color-primary-700: #915808;
--color-primary-800: #603b06;
--color-primary-900: #301d03;
--color-primary-950: #221402;
--color-accent-50: #fee7e9; /* hot fuchsia */
--color-accent-100: #fccfd3;
--color-accent-200: #f99fa6;
--color-accent-300: #f76e7a;
--color-accent-400: #f43e4d;
--color-accent: #f10e21;
--color-accent-600: #c10b1a;
--color-accent-700: #910814;
--color-accent-800: #60060d;
--color-accent-900: #300307;
--color-accent-950: #220205;
--color-cream: #fef4e7; /* simple cream */
--color-cream-100: #fce9cf;
--color-cream-200: #f9d49f;
--color-cream-300: #f7be6e;
--color-cream-400: #f4a83e;
--color-cream-500: #f1920e;
--color-cream-600: #c1750b;
--color-cream-700: #915808;
--color-cream-800: #603b06;
--color-cream-900: #301d03;
--color-cream-950: #221402;
--color-champagne-mist-50: #fef4e7;
--color-champagne-mist-100: #fce9cf;
--color-champagne-mist-200: #fad49e;
--color-champagne-mist-300: #f7be6e;
--color-champagne-mist-400: #f5a83d;
--color-champagne-mist-500: #f2930d;
--color-champagne-mist-600: #c2750a;
--color-champagne-mist-700: #915808;
--color-champagne-mist-800: #613b05;
--color-champagne-mist-900: #301d03;
--color-champagne-mist-950: #221502;
--color-white: #fffdfa;
--color-white-100: #fceacf;
--color-white-200: #fad59e;
--color-white-300: #f7c06e;
--color-white-400: #f5ab3d;
--color-white-500: #f2960d;
--color-white-600: #c2780a;
--color-white-700: #915a08;
--color-white-800: #613c05;
--color-white-900: #301e03;
--color-white-950: #221502;
}
body {
@apply bg-white dark:bg-white-900 dark:text-white;
}

View file

@ -0,0 +1,7 @@
<script lang="ts">
import './layout.css';
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1,7 @@
<script lang="ts">
import Link from '$lib/Link.svelte';
</script>
<h1>Welcome to your library project</h1>
<p>Create your package using @sveltejs/package and preview/showcase your work with SvelteKit</p>
<p>Visit <Link href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</Link> to read the documentation</p>

View file

@ -0,0 +1 @@
@import "tailwindcss";

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
},
};
export default config;

View file

@ -0,0 +1,16 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}

View file

@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
});

View file

@ -24,6 +24,7 @@
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"bowser": "^2.12.1",
"colorjs.io": "^0.5.2",
"virtual-dom": "^2.1.1"
},
"peerDependencies": {

View file

@ -5,7 +5,6 @@ export * from './lint/editorUtils';
export { default as Highlights } from './lint/Highlights';
export { default as LintFramework } from './lint/LintFramework';
export * from './lint/lintKindColor';
export { default as lintKindColor } from './lint/lintKindColor';
export { default as PopupHandler } from './lint/PopupHandler';
export { default as RenderBox } from './lint/RenderBox';
export * from './lint/unpackLint';

View file

@ -17,7 +17,7 @@ import {
getSlateRoot,
getTrixRoot,
} from './editorUtils';
import lintKindColor, { type LintKind } from './lintKindColor';
import { type LintKind, lintKindColor } from './lintKindColor';
import RenderBox from './RenderBox';
import type SourceElement from './SourceElement';
import type { UnpackedLint } from './unpackLint';

View file

@ -5,7 +5,7 @@ import type { VNode } from 'virtual-dom';
import h from 'virtual-dom/h';
import bookDownSvg from '../assets/bookDownSvg';
import type { IgnorableLintBox, LintBox } from './Box';
import lintKindColor from './lintKindColor';
import { type LintKind, lintKindColor, lintKindTextColor } from './lintKindColor';
// Decoupled: actions passed in by framework consumer
import type { UnpackedLint, UnpackedSuggestion } from './unpackLint';
@ -190,6 +190,7 @@ function addToDictionary(
}
function suggestions(
lintKind: LintKind,
suggestions: UnpackedSuggestion[],
apply: (s: UnpackedSuggestion) => void,
): any {
@ -197,7 +198,13 @@ function suggestions(
const label = s.replacement_text !== '' ? s.replacement_text : String(s.kind);
const desc = `Replace with "${label}"`;
const props = i === 0 ? { hook: new FocusHook() } : {};
return button(label, { background: '#2DA44E', color: '#FFFFFF' }, () => apply(s), desc, props);
return button(
label,
{ background: lintKindColor(lintKind), color: lintKindTextColor(lintKind) },
() => apply(s),
desc,
props,
);
});
}
@ -221,10 +228,10 @@ function reportProblemButton(reportError?: () => Promise<void>): any {
);
}
function styleTag() {
function styleTag(lintKind: LintKind) {
return h('style', { id: 'harper-suggestion-style' }, [
`code{
background-color:#e3eccf;
text-decoration: underline solid ${lintKindColor(lintKind)} 2px;
padding:0.125rem;
border-radius:0.25rem
}
@ -351,10 +358,16 @@ function styleTag() {
animation: fadeIn 100ms ease-in-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-color-scheme:dark){
code{background-color:#1f2d3d;color:#c9d1d9}
@ -437,6 +450,7 @@ export default function SuggestionBox(
top: bottom ? '' : `${top}px`,
bottom: bottom ? `${bottom}px` : '',
left: `${left}px`,
transformOrigin: `${bottom ? 'bottom' : 'top'} left`,
};
return h(
@ -447,7 +461,7 @@ export default function SuggestionBox(
'harper-close-on-escape': new CloseOnEscapeHook(close),
},
[
styleTag(),
styleTag(box.lint.lint_kind),
header(
box.lint.lint_kind_pretty,
lintKindColor(box.lint.lint_kind),
@ -458,7 +472,7 @@ export default function SuggestionBox(
),
body(box.lint.message_html),
footer(
suggestions(box.lint.suggestions, (v) => {
suggestions(box.lint.lint_kind, box.lint.suggestions, (v) => {
box.applySuggestion(v);
close();
}),

View file

@ -1,3 +1,5 @@
import { getContrastingTextColor } from './utils';
// First, define the color map as a constant
const LINT_KIND_COLORS = {
Agreement: '#228B22', // Forest green
@ -29,10 +31,15 @@ export type LintKind = keyof typeof LINT_KIND_COLORS;
export const LINT_KINDS = Object.keys(LINT_KIND_COLORS) as LintKind[];
// The main function that uses the map
export default function lintKindColor(lintKindKey: string): string {
export function lintKindColor(lintKindKey: string): string {
const color = LINT_KIND_COLORS[lintKindKey as LintKind];
if (!color) {
throw new Error(`Unexpected lint kind: ${lintKindKey}`);
}
return color;
}
export function lintKindTextColor(lintKindKeyOrColor: string): 'black' | 'white' {
const color = LINT_KIND_COLORS[lintKindKeyOrColor as LintKind] ?? lintKindKeyOrColor;
return getContrastingTextColor(color);
}

View file

@ -0,0 +1,13 @@
import Color from 'colorjs.io';
/** Get the text color that best contrasts with a background of the provided color. */
export function getContrastingTextColor(color: string): 'black' | 'white' {
const c = new Color(color);
const luminance = c.luminance;
if (luminance > 0.5) {
return 'black';
} else {
return 'white';
}
}

View file

@ -19,7 +19,6 @@
"autoprefixer": "^10.4.21",
"drizzle-kit": "^0.31.5",
"flowbite": "^3.1.2",
"flowbite-svelte": "^0.44.18",
"svelte": "^5.15.0",
"svelte-check": "^4.1.5",
"tailwindcss": "^4.1.16",
@ -35,6 +34,7 @@
"@sveltepress/theme-default": "^5.0.7",
"@sveltepress/vite": "^1.1.5",
"chart.js": "^4.4.8",
"components": "workspace:*",
"drizzle-orm": "^0.44.6",
"drizzle-zod": "^0.8.3",
"harper.js": "workspace:*",

View file

@ -1,53 +1,118 @@
@import "tailwindcss";
@import "components/components.css";
@layer base {
body {
@apply bg-white text-black dark:bg-gray-900 dark:text-white;
}
@custom-variant dark (&:where(.dark, .dark *));
ul {
@apply list-disc pl-4;
}
@theme {
--color-primary-50: #fef4e7; /* honey bronze */
--color-primary-100: #fce9cf;
--color-primary-200: #f9d49f;
--color-primary-300: #f7be6e;
--color-primary-400: #f4a83e;
--color-primary: #f1920e;
--color-primary-600: #c1750b;
--color-primary-700: #915808;
--color-primary-800: #603b06;
--color-primary-900: #301d03;
--color-primary-950: #221402;
ol {
@apply list-decimal pl-4;
}
--color-accent-50: #fee7e9; /* hot fuchsia */
--color-accent-100: #fccfd3;
--color-accent-200: #f99fa6;
--color-accent-300: #f76e7a;
--color-accent-400: #f43e4d;
--color-accent: #f10e21;
--color-accent-600: #c10b1a;
--color-accent-700: #910814;
--color-accent-800: #60060d;
--color-accent-900: #300307;
--color-accent-950: #220205;
h1 {
@apply text-4xl font-extrabold tracking-tight lg:text-5xl py-4;
}
--color-cream: #fef4e7; /* simple cream */
--color-cream-100: #fce9cf;
--color-cream-200: #f9d49f;
--color-cream-300: #f7be6e;
--color-cream-400: #f4a83e;
--color-cream-500: #f1920e;
--color-cream-600: #c1750b;
--color-cream-700: #915808;
--color-cream-800: #603b06;
--color-cream-900: #301d03;
--color-cream-950: #221402;
h2 {
@apply text-3xl font-semibold tracking-tight py-4;
}
--color-champagne-mist-50: #fef4e7;
--color-champagne-mist-100: #fce9cf;
--color-champagne-mist-200: #fad49e;
--color-champagne-mist-300: #f7be6e;
--color-champagne-mist-400: #f5a83d;
--color-champagne-mist-500: #f2930d;
--color-champagne-mist-600: #c2750a;
--color-champagne-mist-700: #915808;
--color-champagne-mist-800: #613b05;
--color-champagne-mist-900: #301d03;
--color-champagne-mist-950: #221502;
h3 {
@apply text-2xl font-semibold tracking-tight py-4;
}
h4 {
@apply text-xl font-semibold tracking-tight;
}
p {
@apply leading-7 [&:not(:first-child)]:mt-6;
}
a {
@apply underline-offset-4 underline;
}
blockquote {
@apply mt-6 border-l-2 border-gray-200 pl-6 italic dark:border-gray-700;
}
--color-white: #fffdfa;
--color-white-100: #fceacf;
--color-white-200: #fad59e;
--color-white-300: #f7c06e;
--color-white-400: #f5ab3d;
--color-white-500: #f2960d;
--color-white-600: #c2780a;
--color-white-700: #915a08;
--color-white-800: #613c05;
--color-white-900: #301e03;
--color-white-950: #221502;
}
* {
body {
@apply bg-white text-black dark:bg-black dark:text-white;
font-family:
Atkinson Hyperlegible,
sans-serif;
}
ul {
@apply list-disc pl-4;
}
ol {
@apply list-decimal pl-4;
}
h1 {
@apply text-4xl font-extrabold tracking-tight lg:text-5xl py-4;
font-family: Domine, serif;
}
h2 {
@apply text-3xl font-semibold tracking-tight py-4;
font-family: Domine, serif;
}
h3 {
@apply text-2xl font-semibold tracking-tight py-4;
font-family: Domine, serif;
}
h4 {
@apply text-xl font-semibold tracking-tight;
font-family: Domine, serif;
}
p {
@apply leading-7 [&:not(:first-child)]:mt-6;
}
a {
@apply underline-offset-4 text-black dark:text-white;
}
blockquote {
@apply mt-6 border-l-2 border-gray-200 pl-6 italic dark:border-gray-700;
}
code {
font-family: "JetBrains Mono", monospace;
word-break: keep-all;

View file

@ -70,10 +70,22 @@
<meta property="og:description" content="Harper checks your writing fast, without compromising your privacy." />
<meta property="og:url" content="https://writewithharper.com" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Domine:wght@400..700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="false" class="m-0 p-0 w-full h-full dark:bg-gray-800">
<body data-sveltekit-preload-data="false" class="m-0 p-0 w-full h-full">
<div style="display: contents" class="m-0 p-0 h-full">%sveltekit.body%</div>
</body>

View file

@ -1,5 +1,5 @@
<script>
import { Button } from 'flowbite-svelte';
import { Button } from 'components';
import { binary, LocalLinter } from 'harper.js';
let linter = new LocalLinter({ binary });

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Card } from 'flowbite-svelte';
import { Card } from 'components';
import { type WorkerLinter } from 'harper.js';
import {
type IgnorableLintBox,
@ -119,8 +119,8 @@ function jumpTo(lintBox: IgnorableLintBox) {
}
</script>
<div class="flex flex-row h-full w-full">
<Card class="flex-1 h-full p-5 z-10 max-w-full text-lg mr-5">
<div class="flex flex-row h-full w-full [&_*]:outline-none">
<Card class="flex-1 h-full p-5 z-10 max-w-full text-lg mr-5 bg-white dark:bg-black overflow-auto">
<div bind:this={editor} spellcheck="false">
{@html content.replace(/\n\n/g, '<br>')}
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Spinner } from 'flowbite-svelte';
import { Spinner } from 'components';
export let content: string | undefined = undefined;
let editor = import('./Editor.svelte');

View file

@ -1,5 +1,11 @@
<script lang="ts">
import { lintKindColor, type UnpackedLint, type UnpackedSuggestion } from 'lint-framework';
import { Button } from 'components';
import {
lintKindColor,
lintKindTextColor,
type UnpackedLint,
type UnpackedSuggestion,
} from 'lint-framework';
import { slide } from 'svelte/transition';
export let lint: UnpackedLint;
@ -60,14 +66,16 @@ function suggestionText(s: UnpackedSuggestion): string {
{#if lint.suggestions && lint.suggestions.length > 0}
<div class="flex flex-wrap gap-2 justify-end">
{#each lint.suggestions as s}
<button
class="inline-flex items-center justify-center rounded-md px-2 py-1 text-xs font-semibold"
style="background:#2DA44E;color:#FFFFFF"
<Button
size="xs"
color={lintKindColor(lint.lint_kind)}
textColor={lintKindTextColor(lint.lint_kind)}
class="!px-2 !py-1 text-xs font-semibold"
title={`Replace with \"${suggestionText(s)}\"`}
on:click={() => onApply?.(s)}
>
{suggestionText(s)}
</button>
</Button>
{/each}
</div>
{:else}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Card } from 'flowbite-svelte';
import { Button, Card } from 'components';
import { type IgnorableLintBox, type LintBox, type UnpackedLint } from 'lint-framework';
import LintCard from '$lib/components/LintCard.svelte';
@ -73,25 +73,29 @@ $: if (openSet.size > 0) {
}
</script>
<Card class="hidden md:flex md:flex-col md:w-1/3 h-full p-5 z-10">
<Card class="hidden md:flex md:flex-col md:w-1/3 h-full p-5 z-10 bg-white dark:bg-black">
<div class="flex items-center justify-between mb-3">
<div class="text-base font-semibold">Problems</div>
<div class="flex items-center gap-2">
<button
class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-[#0b0f14]"
<Button
size="xs"
color="light"
class="text-xs"
on:click={toggleAll}
aria-label={allOpen ? 'Collapse all lint cards' : 'Open all lint cards'}
>
{allOpen ? 'Collapse all' : 'Open all'}
</button>
<button
class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-[#0b0f14]"
</Button>
<Button
size="xs"
color="light"
class="text-xs"
on:click={ignoreAll}
disabled={lintBoxes.length === 0}
aria-label="Ignore all current lints"
>
Ignore all
</button>
</Button>
</div>
</div>
<div class="flex-1 overflow-y-auto pr-1">

View file

@ -1,10 +1,12 @@
<script lang="ts">
import { Card } from 'components';
export let authorName: string;
export let authorSubtitle: string;
export let testimonial: string;
</script>
<article class="flex h-full flex-col justify-between gap-6 rounded-xl border border-neutral-200 bg-white p-6 shadow-sm break-inside-avoid dark:border-neutral-800 dark:bg-neutral-900">
<Card class="flex h-full flex-col justify-between gap-6">
<p class="text-base leading-relaxed text-neutral-700 dark:text-neutral-200">
{testimonial}
</p>
@ -12,4 +14,4 @@ export let testimonial: string;
<span class="font-semibold text-neutral-900 dark:text-neutral-50">{authorName}</span>
<span class="text-sm text-neutral-600 dark:text-neutral-400">{authorSubtitle}</span>
</footer>
</article>
</Card>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { Link } from 'components';
import Testimonial from './Testimonial.svelte';
type TestimonialItem = {
@ -20,13 +21,13 @@ const { class: extraClass = '', ...restProps } = $$restProps;
>
<div class="columns-1 gap-6 sm:columns-2 lg:columns-3">
{#each testimonials as item, index (item.authorName + index)}
<a href={item.source} class={`block mb-6 break-inside-avoid no-underline ${index % 2 == 0 ? "skew-hover" : "skew-hover-left"}`}>
<Link href={item.source} class={`block mb-6 break-inside-avoid ${index % 2 == 0 ? "skew-hover" : "skew-hover-left"}`}>
<Testimonial
authorName={item.authorName}
authorSubtitle={item.authorSubtitle}
testimonial={item.testimonial}
/>
</a>
</Link>
{/each}
</div>
</section>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import '../app.css';
import { Link } from 'components';
import posthog from 'posthog-js';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
@ -21,16 +22,6 @@ let names = ['Grammar Guru', 'Grammar Checker', 'Grammar Savior'];
let displayName = names[Math.floor(Math.random() * names.length)];
</script>
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet"
/>
<div class="flex flex-col h-full">
<div class="flex-1">
<GutterCenter>
@ -39,12 +30,12 @@ let displayName = names[Math.floor(Math.random() * names.length)];
</div>
<div class="w-full flex flex-row justify-center h-12">
<a href="https://automattic.com/">
<Link href="https://automattic.com/">
<div class="flex items-center">
An
<div class="inline-block"><AutomatticLogo height="32px" width="140px" /></div>
{displayName}
</div>
</a>
</Link>
</div>
</div>

View file

@ -22,8 +22,8 @@ import SublimeLogo from '$lib/components/SublimeLogo.svelte';
import WordPressLogo from '$lib/components/WordPressLogo.svelte';
import ZedLogo from '$lib/components/ZedLogo.svelte';
import EdgeLogo from '$lib/components/EdgeLogo.svelte';
import { Card, Collapsible, Link } from 'components';
import { browser } from '$app/environment';
import Testimonial from '$lib/components/Testimonial.svelte';
/**
* @param {string} keyword
@ -104,56 +104,56 @@ const testimonials = [
<div class="space-y-2 text-center">
<h1 class="font-bold">Hi. I'm Harper.</h1>
<h2>
The <strong>Free</strong> Grammar Checker That Respects Your Privacy
The <strong class="bg-primary-100 dark:bg-primary-800 p-1 inline-block -rotate-1">Free</strong> Grammar Checker That Respects Your Privacy
</h2>
</div>
<div
class="md:flex md:flex-row grid grid-cols-2 items-center justify-evenly place-items-center gap-2 pt-2 text-center"
>
<a
<Link
href="https://github.com/automattic/harper"
class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
>
<GitHubLogo width="40px" height="40px" />GitHub
</a>
</Link>
{#if agentHas("firefox")}
<a href="https://addons.mozilla.org/en-US/firefox/addon/private-grammar-checker-harper/" class="flex flex-row items-center [&>*]:m-2 skew-hover"
><FirefoxLogo width="40px" height="40px" />Add to Firefox</a
<Link href="https://addons.mozilla.org/en-US/firefox/addon/private-grammar-checker-harper/" class="flex flex-row items-center [&>*]:m-2 skew-hover"
><FirefoxLogo width="40px" height="40px" />Add to Firefox</Link
>
{:else if agentHas("Edg")}
<a href="https://microsoftedge.microsoft.com/addons/detail/private-grammar-checker-/ihjkkjfembmnjldmdchmadigpmapkpdh" class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
><EdgeLogo width="40px" height="40px" />Add to Edge</a
<Link href="https://microsoftedge.microsoft.com/addons/detail/private-grammar-checker-/ihjkkjfembmnjldmdchmadigpmapkpdh" class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
><EdgeLogo width="40px" height="40px" />Add to Edge</Link
>
{:else}
<a href="https://chromewebstore.google.com/detail/private-grammar-checking/lodbfhdipoipcjmlebjbgmmgekckhpfb" class="flex flex-row items-center [&>*]:m-2 skew-hover"
><ChromeLogo width="40px" height="40px" />Add to Chrome</a
<Link href="https://chromewebstore.google.com/detail/private-grammar-checking/lodbfhdipoipcjmlebjbgmmgekckhpfb" class="flex flex-row items-center [&>*]:m-2 skew-hover"
><ChromeLogo width="40px" height="40px" />Add to Chrome</Link
>
{/if}
<a
<Link
href="https://marketplace.visualstudio.com/items?itemName=elijah-potter.harper"
class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
>
<CodeLogo width="40px" height="40px" />Install in VS Code
</a>
<a
</Link>
<Link
href="/docs/integrations/obsidian"
class="flex flex-row items-center [&>*]:m-2 skew-hover"
>
<ObsidianLogo width="40px" height="40px" />Install in Obsidian
</a>
<a href="https://elijahpotter.dev" class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
</Link>
<Link href="https://elijahpotter.dev" class="flex flex-row items-center [&>*]:m-2 skew-hover-left"
><img
width="40"
height="40"
src="/icons/profile.svg"
alt="Author"
/>Author</a
/>Author</Link
>
</div>
<div class="h-[800px] w-full overflow-hidden rounded-xl border border-neutral-200 shadow-sm dark:border-neutral-800">
<div class="h-[800px] w-full">
{#if browser}
<LazyEditor />
{/if}
@ -193,92 +193,112 @@ const testimonials = [
<svelte:fragment slot="title">Native Everywhere</svelte:fragment>
<p>
Harper is available as a
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/language-server">language server</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/harperjs/introduction">JavaScript library</a>
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/language-server">language server</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/harperjs/introduction">JavaScript library</Link>
through WebAssembly, and
<a class="text-blue-600 underline dark:text-blue-400" href="https://crates.io/crates/harper-core">Rust crate</a>,
<Link class="text-blue-600 dark:text-blue-400" href="https://crates.io/crates/harper-core">Rust crate</Link>,
so you can get fantastic grammar checking anywhere you work.
</p>
<p>
That said, we take extra care to make sure the
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/visual-studio-code">Visual Studio Code</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/neovim">Neovim</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/obsidian">Obsidian</a>, and
<a class="text-blue-600 underline dark:text-blue-400" href="https://chromewebstore.google.com/detail/private-grammar-checking/lodbfhdipoipcjmlebjbgmmgekckhpfb">Chrome</a>
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/visual-studio-code">Visual Studio Code</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/neovim">Neovim</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/obsidian">Obsidian</Link>, and
<Link class="text-blue-600 dark:text-blue-400" href="https://chromewebstore.google.com/detail/private-grammar-checking/lodbfhdipoipcjmlebjbgmmgekckhpfb">Chrome</Link>
extensions are amazing.
</p>
<svelte:fragment slot="aside">
<div class="grid gap-4 sm:grid-cols-2">
<a
<div class="grid gap-2 sm:grid-cols-2">
<Link
href="/docs/integrations/obsidian"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover-left"
class="skew-hover-left"
>
<ObsidianLogo width="40" height="40" />
<span class="font-medium">Obsidian</span>
</a>
<a
<Card class="flex items-center gap-3">
<ObsidianLogo width="40" height="40" />
<span class="font-medium">Obsidian</span>
</Card>
</Link>
<Link
href="/docs/integrations/visual-studio-code"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover"
class="skew-hover"
>
<CodeLogo width="40" height="40" />
<span class="font-medium">Visual Studio Code</span>
</a>
<a
<Card class="flex items-center gap-3">
<CodeLogo width="40" height="40" />
<span class="font-medium">Visual Studio Code</span>
</Card>
</Link>
<Link
href="/docs/integrations/neovim"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover"
class="skew-hover"
>
<NeovimLogo width="40" height="40" />
<span class="font-medium">Neovim</span>
</a>
<a
<Card class="flex items-center gap-3">
<NeovimLogo width="40" height="40" />
<span class="font-medium">Neovim</span>
</Card>
</Link>
<Link
href="https://chromewebstore.google.com/detail/private-grammar-checking/lodbfhdipoipcjmlebjbgmmgekckhpfb"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover-left"
class="skew-hover-left"
>
<ChromeLogo width="40" height="40" />
<span class="font-medium">Chrome</span>
</a>
<a
<Card class="flex items-center gap-3">
<ChromeLogo width="40" height="40" />
<span class="font-medium">Chrome</span>
</Card>
</Link>
<Link
href="https://addons.mozilla.org/en-US/firefox/addon/private-grammar-checker-harper/"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover"
class="skew-hover"
>
<FirefoxLogo width="40" height="40" />
<span class="font-medium">Firefox</span>
</a>
<a
<Card class="flex items-center gap-3">
<FirefoxLogo width="40" height="40" />
<span class="font-medium">Firefox</span>
</Card>
</Link>
<Link
href="/docs/integrations/helix"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover-left"
class="skew-hover-left"
>
<HelixLogo width="40" height="40" />
<span class="font-medium">Helix</span>
</a>
<a
<Card class="flex items-center gap-3">
<HelixLogo width="40" height="40" />
<span class="font-medium">Helix</span>
</Card>
</Link>
<Link
href="/docs/integrations/wordpress"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover-left"
class="skew-hover-left"
>
<WordPressLogo width="40" height="40" />
<span class="font-medium">WordPress</span>
</a>
<a
<Card class="flex items-center gap-3">
<WordPressLogo width="40" height="40" />
<span class="font-medium">WordPress</span>
</Card>
</Link>
<Link
href="/docs/integrations/zed"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover"
class="skew-hover"
>
<ZedLogo width="40" height="40" />
<span class="font-medium">Zed</span>
</a>
<a
<Card class="flex items-center gap-3">
<ZedLogo width="40" height="40" />
<span class="font-medium">Zed</span>
</Card>
</Link>
<Link
href="/docs/integrations/emacs"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover-left"
class="skew-hover-left"
>
<EmacsLogo width="40" height="40" />
<span class="font-medium">Emacs</span>
</a>
<a
<Card class="flex items-center gap-3">
<EmacsLogo width="40" height="40" />
<span class="font-medium">Emacs</span>
</Card>
</Link>
<Link
href="/docs/integrations/sublime-text"
class="flex items-center gap-3 rounded-lg border border-neutral-200 px-4 py-3 shadow-sm transition hover:shadow-md dark:border-neutral-800 skew-hover"
class="skew-hover"
>
<SublimeLogo width="40" height="40" />
<span class="font-medium">Sublime Text</span>
</a>
<Card class="flex items-center gap-3">
<SublimeLogo width="40" height="40" />
<span class="font-medium">Sublime Text</span>
</Card>
</Link>
</div>
</svelte:fragment>
</Section>
@ -290,9 +310,9 @@ const testimonials = [
</p>
<p>No network request, no massive language models, no fuss.</p>
<svelte:fragment slot="aside">
<div class="rounded-xl border border-neutral-200 p-4 shadow-sm dark:border-neutral-800">
<Card>
<Graph />
</div>
</Card>
</svelte:fragment>
</Section>
@ -304,119 +324,89 @@ const testimonials = [
<Section id="faqs">
<svelte:fragment slot="title">FAQs</svelte:fragment>
<div class="space-y-4">
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
Is Harper Free?
</summary>
<p class="mt-3">
<Collapsible title="Is Harper Free?">
<p>
Yes. Harper is free in every sense of the word. You don't need a credit card to start using
Harper, and the source code is freely available under the Apache-2.0 license.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
How Does Harper Work?
</summary>
<p class="mt-3">
</Collapsible>
<Collapsible title="How Does Harper Work?">
<p>
Harper watches your writing and provides instant suggestions when it notices a grammatical
error. When you see an underline, it's probably because Harper has something to say.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
Does Harper Change The Meaning of My Words?
</summary>
<p class="mt-3">
</Collapsible>
<Collapsible title="Does Harper Change The Meaning of My Words?">
<p>
No. Harper will never intentionally suggest an edit that might change your meaning. Harper
strives to never make it harder to express your creativity.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
Is Harper Really Private?
</summary>
<p class="mt-3">
</Collapsible>
<Collapsible title="Is Harper Really Private?">
<p>
Harper is the only widespread and comprehensive grammar checker that is truly private. Your
data never leaves your device. Your writing should remain just that: <strong>yours.</strong>
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
How Do I Use or Integrate Harper?
</summary>
<div class="mt-3">
</Collapsible>
<Collapsible title="How Do I Use or Integrate Harper?">
<div class="space-y-3">
<p>
That depends on your use case. Do you want to use it within Obsidian? We have an
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/obsidian">Obsidian plugin</a>. Do you want to use it within WordPress? We have a
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/wordpress">WordPress plugin</a>. Do you want to use it within your Browser? We have a
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/chrome-extension">Chrome extension</a> and a
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/firefox-extension">Firefox plugin</a>. Do you want to use it within your code editor? We have documentation on how you can integrate with
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/visual-studio-code">Visual Studio Code and its forks</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/neovim">Neovim</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/helix">Helix</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/emacs">Emacs</a>,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/zed">Zed</a> and
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/sublime-text">Sublime Text</a>. If you're using a different code editor, then you can integrate directly with our language server,
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/language-server">harper-ls</a>. Do you want to integrate it in your web app or your JavaScript/TypeScript codebase? You can use
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/harperjs/introduction">harper.js</a>. Do you want to integrate it in your Rust program or codebase? You can use
<a class="text-blue-600 underline dark:text-blue-400" href="https://crates.io/crates/harper-core">harper-core</a>.
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/obsidian">Obsidian plugin</Link>. Do you want to use it within WordPress? We have a
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/wordpress">WordPress plugin</Link>. Do you want to use it within your Browser? We have a
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/chrome-extension">Chrome extension</Link> and a
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/firefox-extension">Firefox plugin</Link>. Do you want to use it within your code editor? We have documentation on how you can integrate with
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/visual-studio-code">Visual Studio Code and its forks</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/neovim">Neovim</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/helix">Helix</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/emacs">Emacs</Link>,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/zed">Zed</Link> and
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/sublime-text">Sublime Text</Link>. If you're using a different code editor, then you can integrate directly with our language server,
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/language-server">harper-ls</Link>. Do you want to integrate it in your web app or your JavaScript/TypeScript codebase? You can use
<Link class="text-blue-600 dark:text-blue-400" href="/docs/harperjs/introduction">harper.js</Link>. Do you want to integrate it in your Rust program or codebase? You can use
<Link class="text-blue-600 dark:text-blue-400" href="https://crates.io/crates/harper-core">harper-core</Link>.
</p>
</div>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
What Human Languages Do You Support?
</summary>
<p class="mt-3">
</Collapsible>
<Collapsible title="What Human Languages Do You Support?">
<p>
We currently only support English and its dialects British, American, Canadian, and
Australian. Other languages are on the horizon, but we want our English support to be truly
amazing before we diversify.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
What Programming Languages Do You Support?
</summary>
<p class="mt-3">
For <code>harper-ls</code> and our code editor integrations, we support a wide variety of
programming languages. You can view all of them over at the
<a class="text-blue-600 underline dark:text-blue-400" href="/docs/integrations/language-server#Supported-Languages">harper-ls documentation</a>.
We are entirely open to PRs that add support. If you just want to be able to run grammar checking
on your code's comments, you can use
<a class="text-blue-600 underline dark:text-blue-400" href="https://github.com/Automattic/harper/pull/332">this PR as a model for what to do</a>.
</p>
<p class="mt-3">
For <code>harper.js</code> and those that use it under the hood like our Obsidian plugin, we
support plaintext and/or Markdown.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
Where Did the Name Harper Come From?
</summary>
<p class="mt-3">
See <a class="text-blue-600 underline dark:text-blue-400" href="https://elijahpotter.dev/articles/naming_harper">this blog post</a>.
</p>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
Do I Need a GPU?
</summary>
<div class="mt-3">
<p>No. Harper runs on-device, no matter what. There are no special hardware requirements. No GPU, no additional memory, no fuss.</p>
</Collapsible>
<Collapsible title="What Programming Languages Do You Support?">
<div class="space-y-3">
<p>
For <code>harper-ls</code> and our code editor integrations, we support a wide variety of
programming languages. You can view all of them over at the
<Link class="text-blue-600 dark:text-blue-400" href="/docs/integrations/language-server#Supported-Languages">harper-ls documentation</Link>.
We are entirely open to PRs that add support. If you just want to be able to run grammar checking
on your code's comments, you can use
<Link class="text-blue-600 dark:text-blue-400" href="https://github.com/Automattic/harper/pull/332">this PR as a model for what to do</Link>.
</p>
<p>
For <code>harper.js</code> and those that use it under the hood like our Obsidian plugin, we
support plaintext and/or Markdown.
</p>
</div>
</details>
<details class="group rounded-lg border border-neutral-200 bg-white p-4 shadow-sm open:shadow-md dark:border-neutral-800 dark:bg-neutral-900">
<summary class="cursor-pointer font-semibold marker:text-neutral-400">
What Do I Do If My Question Isn't Here?
</summary>
<p class="mt-3">
You can join our
<a class="text-blue-600 underline dark:text-blue-400" href="https://discord.gg/invite/JBqcAaKrzQ">Discord</a>
and ask your questions there or you can start a discussion over at
<a class="text-blue-600 underline dark:text-blue-400" href="https://github.com/Automattic/harper/discussions">GitHub</a>.
</Collapsible>
<Collapsible title="Where Did the Name Harper Come From?">
<p>
See <Link class="text-blue-600 dark:text-blue-400" href="https://elijahpotter.dev/articles/naming_harper">this blog post</Link>.
</p>
</details>
</Collapsible>
<Collapsible title="Do I Need a GPU?">
<p>No. Harper runs on-device, no matter what. There are no special hardware requirements. No GPU, no additional memory, no fuss.</p>
</Collapsible>
<Collapsible title="What Do I Do If My Question Isn't Here?">
<p>
You can join our
<Link class="text-blue-600 dark:text-blue-400" href="https://discord.gg/invite/JBqcAaKrzQ">Discord</Link>
and ask your questions there or you can start a discussion over at
<Link class="text-blue-600 dark:text-blue-400" href="https://github.com/Automattic/harper/discussions">GitHub</Link>.
</p>
</Collapsible>
</div>
</Section>
@ -425,7 +415,7 @@ const testimonials = [
<p>Harper is completely open source under the Apache-2.0 license.</p>
<p>
Come pay us a visit on
<a class="text-blue-600 underline dark:text-blue-400" href="https://github.com/automattic/harper">GitHub</a>.
<Link class="text-blue-600 dark:text-blue-400" href="https://github.com/automattic/harper">GitHub</Link>.
</p>
</Section>
</main>

View file

@ -3,7 +3,7 @@ title: Harper for WordPress
---
<script>
import {Button} from "flowbite-svelte"
import {Button} from "components"
</script>
Harper still works great with WordPress, but the recommended way to use it today is the

View file

@ -6,7 +6,7 @@ import {
TableBodyRow,
TableHead,
TableHeadCell,
} from 'flowbite-svelte';
} from 'components';
import { binary, type LintConfig, LocalLinter } from 'harper.js';
export const frontmatter = {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Textarea } from 'flowbite-svelte';
import { Link, Textarea } from 'components';
let demoText =
'Ths is an text box you can type in.\n\nany other site on the web will work the the same!';
@ -66,7 +66,7 @@ let demoText =
If you work somewhere that isn't on our list of supported sites, you can enable the Chrome extension anyway by opening the Harper extension popup and clicking the power button.
<br/>
<br/>
Alternatively, <a href="/request-browser-support">let us know</a> which sites you want us to support and we'll add it as soon as we can.
Alternatively, <Link href="/request-browser-support">let us know</Link> which sites you want us to support and we'll add it as soon as we can.
</p>
<img

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Select, Textarea } from 'flowbite-svelte';
import { Select, Textarea } from 'components';
import { binary, WorkerLinter } from 'harper.js';
import demoText from '../../../../../demo.md?raw';

View file

@ -1,6 +1,7 @@
<script lang="ts">
import 'reveal.js/dist/reveal.css';
import 'reveal.js/dist/theme/serif.css';
import { Link } from 'components';
import Reveal from 'reveal.js';
import { onMount } from 'svelte';
import Logo from '$lib/components/Logo.svelte';
@ -111,7 +112,7 @@ onMount(() => {
<section>
<h2>Try It!</h2>
<p>
Go to <a href="https://writewithharper.com">https://writewithharper.com</a> on a laptop.
Go to <Link href="https://writewithharper.com">https://writewithharper.com</Link> on a laptop.
<img
alt="The QR code for the website."
src=" https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=https://writewithharper.com"

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Card, Checkbox, Input, Label, Radio } from 'flowbite-svelte';
import { Button, Card, Checkbox, Input, Label, Radio } from 'components';
import Isolate from '$lib/components/Isolate.svelte';
</script>

View file

@ -7,7 +7,7 @@ import {
TableBodyRow,
TableHead,
TableHeadCell,
} from 'flowbite-svelte';
} from 'components';
import { binary, type Summary, WorkerLinter } from 'harper.js';
import LintKindChart from '$lib/components/LintKindChart.svelte';

View file

@ -1,5 +1,5 @@
<script>
import { Textarea } from 'flowbite-svelte';
import { Link, Textarea } from 'components';
import { binary, WorkerLinter } from 'harper.js';
import { onMount } from 'svelte';
import Typed from 'typed.js';
@ -62,13 +62,13 @@ onMount(() => {
<div class="text-sm mb-3">By John Doe, Staff Writer</div>
<p class="leading-relaxed">
<a href="/">Harper</a> ships out-of-the box with everything you need to perform complex operations
<Link href="/">Harper</Link> ships out-of-the box with everything you need to perform complex operations
on English text at the edge. That includes converting text to title-case.
</p>
<p class="leading-relaxed">
Just enter your text in the heading above and it'll be converted to title case following
the <a href="https://www.chicagomanualofstyle.org/home.html">Chicago Style</a>. Your
the <Link href="https://www.chicagomanualofstyle.org/home.html">Chicago Style</Link>. Your
privacy means something. Keep your data where you want it: in your hands and on your
device.
</p>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Card, Input, Label, Radio } from 'flowbite-svelte';
import { Button, Card, Input, Label, Radio } from 'components';
import Isolate from '$lib/components/Isolate.svelte';
const reasons = {
@ -51,4 +51,3 @@ function handleFormData(e: FormDataEvent) {
</Card>
</div>
</Isolate>

View file

@ -1,28 +1,4 @@
import flowbitePlugin from 'flowbite/plugin';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
'./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}',
],
plugins: [flowbitePlugin],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
900: '#133f71',
800: '#355280',
700: '#50658f',
600: '#69799f',
500: '#818eae',
400: '#9aa4be',
300: '#b3bace',
200: '#ccd0de',
100: '#e5e7ef',
50: '#ffffff',
},
},
},
},
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/components/**/*.{html,js,svelte,ts}'],
plugins: [],
};

View file

@ -34,8 +34,8 @@ export default defineConfig(async () => {
github: 'https://github.com/automattic/harper',
discord: 'https://discord.gg/invite/JBqcAaKrzQ',
themeColor: {
primary: '#818eae',
dark: '#355280',
primary: '#f1920e',
dark: '#301d03',
gradient: {
start: '#355280',
end: '#818eae',

1502
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -18,3 +18,4 @@ onlyBuiltDependencies:
- keytar
- msw
- svelte-preprocess
- '@tailwindcss/oxide'