โ All Skills Knowledge History
Notebook lined-paper UI for the Memo create/edit modal
memoui-stylinglined-paper-textareaunocssnative-css-src-assetsrule-13-ui-priorityshadow-dom-limitationcss-variable-theming
Request
- The basic add/edit memo form is too plain โ make the modal look like a real memo: a borderless 'blank' title and a lined notebook-paper writing area, with a pushpin and category tint.
- Write the finding to Mono Skills.
Summary
Notebook lined-paper UI for the Memo modal (mono-vue remote)
How to give a form a realistic "notebook paper" look inside a mono-modal, the right
way under Template Rule 13, and the one trick that makes text sit on the lines.
Goal
The plain stacked Add/Edit memo form (mono-input + mono-textarea + select + mono-switch) looked too basic. Turn the modal body into a sheet of ruled notebook paper: a borderless "blank" title line, blue horizontal rules with the content resting on them, a red left margin, a pushpin to pin, and the category tinting the page.
Why native <input>/<textarea> here (Rule 13)
mono-textarea / mono-input are shadow-DOM Lit web components โ you cannot paint a
ruled-line background onto the field inside their shadow root from outside. Rule 13's UI
priority is: mono component โ its .mono-* CSS โ UnoCSS โ native CSS in src/assets/,
imported only into the page that needs it. A lined-paper field is exactly that last-resort
case, so the writing surface uses native <input> + <textarea> styled from a scoped
stylesheet. We still keep mono-modal (the popup shell) and mono-button (footer).
Native elements also simplify binding: plain v-model="store.input.title" on the reactive
store object โ no need for the mono-* CustomEvent reader ($event.detail.modelValue). The
search box stays a mono-input and still uses that reader.
The key trick โ text that rests on the lines
A lined textarea = line-height matched to a repeating-linear-gradient, with
background-attachment: local so the rules scroll with the text:
.memo-paper__content {
--memo-line: 30px; /* MUST equal line-height */
line-height: var(--memo-line);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent calc(var(--memo-line) - 1px),
#c9d6e8 calc(var(--memo-line) - 1px),
#c9d6e8 var(--memo-line)
);
background-attachment: local; /* rules scroll WITH the text, not fixed */
border: none; outline: none; resize: vertical;
background-color: transparent;
}
If the rules and text drift, adjust --memo-line and the field's top padding together โ the
text baseline must land just above each gradient rule. background-attachment: local is what
keeps the rules aligned while the textarea scrolls.
The rest of the sheet
- Scope everything under a
.memo-paperroot class. The stylesheet is a globalimport(import '@my-memo/assets/memo-paper.css'), so namespacing under one root prevents leakage into other pages / the federated example routes. Verified the list + other pages are unchanged. - Red margin =
.memo-paper::beforeabsolute left rule at--memo-margin(44px); fields getpadding-left: calc(var(--memo-margin) + 12px)to clear it. - Borderless title = native
<input>withborder: none; background: transparent, a heavierborder-bottom, bold + a handwriting font stack (Segoe Print/Bradley Hand/Comic Sans MS). - Pushpin = an absolute top-right
<button>togglingstore.input.pinned; icon swapsi-mdi-pinโi-mdi-pin-outline(UnoCSS presetIcons /@iconify/jsonmdi), tilts + tints when pinned. - Category tint = a single CSS variable
--memo-accent, set inline from amemoCategoryAccent: Record<string,string>map kept insrc/datas/tables/memo.ts(Rule 9). The paper root uses the selected category's accent (margin + pin); each category pill sets its own accent so the pill shows its color; the active pill fills with it.
Files
src/assets/memo-paper.css(new) โ all paper styling, namespaced under.memo-paper.src/datas/tables/memo.tsโ addedmemoCategoryAccent(static data in datas, Rule 9).src/pages/memo/index.vueโ modal body swapped to the.memo-paperstructure; dropped the now-unusedmono-helper/ui/textarea+mono-helper/ui/switchimports; added the CSS import.
Gotchas worth keeping
color-mix(in srgb, var(--memo-accent) 60%, #cbd2dd)gives per-category pill/border tints from one accent var (modern browsers / Vite dev โ fine here).- Importing a plain
.cssfile in an SFC injects it globally (not<style scoped>), so the root-class namespacing is what keeps it contained โ that's the Rule 13-compliant pattern (file insrc/assets, imported only by the one page).
Outcome
successvite dev server + module transform + UnoCSS icon check GET /memo, the page module, and /src/assets/memo-paper.css all return 200 (no compile error) Compiled page references .memo-paper, .memo-paper__title/__content, memoCategoryAccent UnoCSS generates i-mdi-pin and i-mdi-pin-outline for the pushpin toggle No leftover mono-textarea / mono-switch references after the swap
Decisions
Native input/textarea for the lined paper, not mono-textarea
Built the writing surface from native <input>/<textarea> styled by a scoped src/assets stylesheet; kept mono-modal + mono-button.
Why: mono-textarea is a shadow-DOM web component โ its internal field can't receive a ruled-line background from outside. Rule 13's last tier (native CSS in src/assets, imported only into the page) is the sanctioned path for this. Native elements also bind with plain v-model on the reactive store.
Lined effect via line-height-matched repeating-linear-gradient + background-attachment: local
One CSS var --memo-line drives both line-height and the gradient period; background-attachment: local keeps rules aligned while the textarea scrolls.
Why: This is the reliable way to make typed text rest on ruled lines that move with the content; mismatched line-height/gradient is what makes text drift off the lines.
Scope all paper CSS under a .memo-paper root class
Namespaced every selector under .memo-paper instead of using <style scoped> or global rules.
Why: A plain CSS import in an SFC is injected globally; with federated example routes also present, root-class namespacing prevents style leakage. Verified other pages and the memo list are unchanged.
Category accent as a single CSS variable from a datas map
memoCategoryAccent in src/datas/tables/memo.ts; set --memo-accent inline; margin, pin, and selected pill all read it.
Why: Keeps static data in datas (Rule 9) and themes the whole sheet from one variable, so adding a category is a one-line change.
Files changed
| File | Operation | Note |
|---|---|---|
src/assets/memo-paper.css | added | Notebook lined-paper styling scoped under .memo-paper: ruled-line gradient textarea, red margin, borderless title, pushpin, category pills; tinted via --memo-accent |
src/datas/tables/memo.ts | modified | Added memoCategoryAccent: Record<string,string> (per-category hex accents) โ static data in datas (Rule 9) |
src/pages/memo/index.vue | modified | Swapped modal body to native .memo-paper structure (v-model title/textarea, pushpin toggle, category pills); imported memo-paper.css + memoCategoryAccent; removed now-unused mono-helper/ui/textarea + /switch imports |
Commands
pnpm dev
curl /memo, /src/pages/memo/index.vue, /src/assets/memo-paper.css (all 200)
MONO_SKILLS=true MONO_SKILLS_GITHUB_TOKEN=[REDACTED] pnpm exec mono skills save --app my-memo --dir .mono/skills/pending/<id>Validation
Tests: not run Build: passed