mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
feat(chrome-ext): show hints randomly (#1909)
Co-authored-by: ethqnol <ethqnol@users.noreply.github.com>
This commit is contained in:
parent
cecadad180
commit
85a8efa2e9
6 changed files with 94 additions and 5 deletions
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: “–”
|
||||
|
||||
|
|
|
|||
13
packages/lint-framework/src/assets/hints.json
Normal file
13
packages/lint-framework/src/assets/hints.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[
|
||||
"Vary sentence length to keep readers engaged.",
|
||||
"Replace clichés with fresh, precise wording.",
|
||||
"Check subject‑verb 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)."
|
||||
]
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue