feat(chrome-ext): show hints randomly (#1909)

Co-authored-by: ethqnol <ethqnol@users.noreply.github.com>
This commit is contained in:
Elijah Potter 2025-09-12 09:15:46 -06:00 committed by GitHub
parent cecadad180
commit 85a8efa2e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 94 additions and 5 deletions

View file

@ -42,14 +42,14 @@ impl ExprLinter for Dashes {
span,
lint_kind,
suggestions: vec![Suggestion::ReplaceWith(vec![EN_DASH])],
message: "A sequence of hyphens is not an en dash.".to_owned(),
message: "Replace these two hyphens with an en dash ().".to_owned(),
priority: 63,
}),
3 => Some(Lint {
span,
lint_kind,
suggestions: vec![Suggestion::ReplaceWith(vec![EM_DASH])],
message: "A sequence of hyphens is not an em dash.".to_owned(),
message: "Replace these three hyphens with an em dash (—).".to_owned(),
priority: 63,
}),
4.. => None, // Ignore longer hyphen sequences.
@ -58,7 +58,7 @@ impl ExprLinter for Dashes {
}
fn description(&self) -> &'static str {
"Rather than outright using an em dash or en dash, authors often use a sequence of hyphens, expecting them to be condensed. Use two hyphens to denote an en dash and three to denote an em dash."
"Writers often type `--` or `---` expecting their editor to convert them into proper dashes. Replace these sequences with the correct characters: use an en dash () for ranges or connections and an em dash (—) for a break in thought."
}
}

View file

@ -1156,7 +1156,7 @@ Message: |
Lint: Formatting (63 priority)
Message: |
455 | Oath or Affirmation:-- "I do solemnly swear (or affirm) that I will faithfully
| ^~ A sequence of hyphens is not an en dash.
| ^~ Replace these two hyphens with an en dash ().
Suggest:
- Replace with: “–”

View file

@ -0,0 +1,13 @@
[
"Vary sentence length to keep readers engaged.",
"Replace clichés with fresh, precise wording.",
"Check subjectverb agreement, especially in long sentences.",
"Keep consistent terminology across the document.",
"Read aloud to catch awkward phrasing.",
"You can easily write em-dashes by typing out three hyphens in a row.",
"You can easily write en-dashes by typing out two hyphens in a row.",
"Harper can be configured to open with a hotkey. Check the extension's settings.",
"Don't agree with a suggestion? Click 'Ignore' and Harper will adapt to your writing style.",
"You can add specialized terms, names, or acronyms to your personal dictionary in Harper's settings.",
"Check your language preferences in Settings to get suggestions for different dialects (e.g., American vs. British English)."
]

View file

@ -4,6 +4,7 @@ import { getCaretPosition } from './editorUtils';
type ActivationKey = 'off' | 'shift' | 'control';
import hintsData from '../assets/hints.json';
import RenderBox from './RenderBox';
import SuggestionBox from './SuggestionBox';
@ -29,6 +30,8 @@ function monitorActivationKey(
export default class PopupHandler {
private currentLintBoxes: IgnorableLintBox[];
private popupLint: number | undefined;
private currentHint: string | null | undefined;
private currentHintFor: number | undefined;
private renderBox: RenderBox;
private pointerDownCallback: (e: PointerEvent) => void;
private activationKeyListener: (() => void) | undefined;
@ -45,6 +48,8 @@ export default class PopupHandler {
}) {
this.actions = actions;
this.currentLintBoxes = [];
this.currentHint = undefined;
this.currentHintFor = undefined;
this.renderBox = new RenderBox(document.body);
this.renderBox.getShadowHost().popover = 'manual';
this.renderBox.getShadowHost().style.pointerEvents = 'none';
@ -104,10 +109,14 @@ export default class PopupHandler {
private render() {
let tree = h('div', {}, []);
this.updateHint();
if (this.popupLint != null && this.popupLint < this.currentLintBoxes.length) {
const box = this.currentLintBoxes[this.popupLint];
tree = SuggestionBox(box, this.actions, () => {
tree = SuggestionBox(box, this.actions, this.currentHint ?? null, () => {
this.popupLint = undefined;
this.updateHint();
});
this.renderBox.getShadowHost().showPopover();
} else {
@ -117,6 +126,27 @@ export default class PopupHandler {
this.renderBox.render(tree);
}
/** Synchronize the hint with the currently focused lint.
* - If no lint is open, clear the hint state.
* - If a different lint opens, or the hint is uninitialized, decide once (~10%).
*/
private updateHint() {
if (this.popupLint == null) {
this.currentHint = undefined;
this.currentHintFor = undefined;
return;
}
if (this.currentHintFor !== this.popupLint || this.currentHint === undefined) {
const hints: string[] = Array.isArray(hintsData)
? ((hintsData as unknown[]).filter((v) => typeof v === 'string') as string[])
: [];
const show = Math.random() < 0.1 && hints.length > 0;
this.currentHint = show ? hints[Math.floor(Math.random() * hints.length)] : null;
this.currentHintFor = this.popupLint;
}
}
public updateLintBoxes(boxes: IgnorableLintBox[]) {
this.currentLintBoxes.forEach((b) => {
b.source.removeEventListener('pointerdown', this.pointerDownCallback as EventListener);

View file

@ -123,6 +123,19 @@ function footer(leftChildren: any, rightChildren: any) {
return h('div', { className: 'harper-footer' }, [left, right]);
}
function hintDrawer(hint: string | null): any {
if (!hint) return undefined;
return h('div', { className: 'harper-hint-drawer', role: 'note', 'aria-live': 'polite' }, [
h('div', { className: 'harper-hint-content' }, [
h('div', { className: 'harper-hint-icon', 'aria-hidden': 'true' }, '💡'),
h('div', {}, [
h('div', { className: 'harper-hint-title' }, 'Tip'),
h('div', {}, String(hint)),
]),
]),
]);
}
function addToDictionary(
box: LintBox,
addToUserDictionary?: (words: string[]) => Promise<void>,
@ -229,6 +242,33 @@ function styleTag() {
gap:16px
}
/* Hint drawer styles */
.harper-hint-drawer{
margin-top:6px;
border-top:1px solid #eaeef2;
background:#f6f8fa;
color:#3e4c59;
border-radius:0 0 6px 6px;
}
.harper-hint-content{
display:flex;
gap:8px;
align-items:flex-start;
padding:8px 10px;
font-size:13px;
line-height:18px;
}
.harper-hint-icon{
flex:0 0 auto;
width:18px;height:18px;
border-radius:50%;
background:#fff3c4;
color:#7c5e10;
display:flex;align-items:center;justify-content:center;
font-weight:700;
}
.harper-hint-title{ font-weight:600; margin-right:6px; color:#1f2328; }
.fade-in {
animation: fadeIn 100ms ease-in-out forwards;
}
@ -261,6 +301,9 @@ function styleTag() {
background:#4b4b4b;
color:#ffffff
}
.harper-hint-drawer{ border-top-color:#30363d; background:#151b23; color:#9aa4af; }
.harper-hint-icon{ background:#3a2f0b; color:#f2cc60; }
.harper-hint-title{ color:#e6edf3; }
}`,
]);
}
@ -280,6 +323,7 @@ export default function SuggestionBox(
openOptions?: () => Promise<void>;
addToUserDictionary?: (words: string[]) => Promise<void>;
},
hint: string | null,
close: () => void,
) {
const top = box.y + box.height + 3;
@ -325,6 +369,7 @@ export default function SuggestionBox(
box.ignoreLint ? ignoreLint(box.ignoreLint) : undefined,
],
),
hintDrawer(hint),
],
);
}

View file

@ -5,6 +5,7 @@
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"resolveJsonModule": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,