mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
feat: create component library + use in relevant places (#2229)
This commit is contained in:
parent
da9e2ba0d6
commit
1377ab51a5
64 changed files with 2163 additions and 823 deletions
|
|
@ -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}
|
||||
|
|
|
|||
21
justfile
21
justfile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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('; ')};`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
24
packages/components/.gitignore
vendored
Normal 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-*
|
||||
1
packages/components/.npmrc
Normal file
1
packages/components/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
58
packages/components/package.json
Normal file
58
packages/components/package.json
Normal 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
13
packages/components/src/app.d.ts
vendored
Normal 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 {};
|
||||
12
packages/components/src/app.html
Normal file
12
packages/components/src/app.html
Normal 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>
|
||||
101
packages/components/src/lib/Button.svelte
Normal file
101
packages/components/src/lib/Button.svelte
Normal 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}
|
||||
16
packages/components/src/lib/Card.svelte
Normal file
16
packages/components/src/lib/Card.svelte
Normal 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>
|
||||
17
packages/components/src/lib/Collapsible.svelte
Normal file
17
packages/components/src/lib/Collapsible.svelte
Normal 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>
|
||||
35
packages/components/src/lib/Input.svelte
Normal file
35
packages/components/src/lib/Input.svelte
Normal 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}
|
||||
/>
|
||||
34
packages/components/src/lib/Link.svelte
Normal file
34
packages/components/src/lib/Link.svelte
Normal 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>
|
||||
45
packages/components/src/lib/Select.svelte
Normal file
45
packages/components/src/lib/Select.svelte
Normal 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>
|
||||
22
packages/components/src/lib/Textarea.svelte
Normal file
22
packages/components/src/lib/Textarea.svelte
Normal 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>
|
||||
23
packages/components/src/lib/index.ts
Normal file
23
packages/components/src/lib/index.ts
Normal 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';
|
||||
74
packages/components/src/lib/styles.css
Normal file
74
packages/components/src/lib/styles.css
Normal 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;
|
||||
}
|
||||
7
packages/components/src/routes/+layout.svelte
Normal file
7
packages/components/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import './layout.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
7
packages/components/src/routes/+page.svelte
Normal file
7
packages/components/src/routes/+page.svelte
Normal 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>
|
||||
1
packages/components/src/routes/layout.css
Normal file
1
packages/components/src/routes/layout.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import "tailwindcss";
|
||||
1
packages/components/static/favicon.svg
Normal file
1
packages/components/static/favicon.svg
Normal 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 |
18
packages/components/svelte.config.js
Normal file
18
packages/components/svelte.config.js
Normal 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;
|
||||
16
packages/components/tsconfig.json
Normal file
16
packages/components/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
packages/components/vite.config.ts
Normal file
7
packages/components/vite.config.ts
Normal 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()],
|
||||
});
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
13
packages/lint-framework/src/lint/utils.ts
Normal file
13
packages/lint-framework/src/lint/utils.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
TableBodyRow,
|
||||
TableHead,
|
||||
TableHeadCell,
|
||||
} from 'flowbite-svelte';
|
||||
} from 'components';
|
||||
import { binary, type LintConfig, LocalLinter } from 'harper.js';
|
||||
|
||||
export const frontmatter = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
1502
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -18,3 +18,4 @@ onlyBuiltDependencies:
|
|||
- keytar
|
||||
- msw
|
||||
- svelte-preprocess
|
||||
- '@tailwindcss/oxide'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue