Settings → Plugins adds a plugin by npm package name or by a path, and the
path had to be typed in full: the market's field was the only way in, with
nothing to pick the folder from. And a plugin added from a folder had no page
of its own — its README comes from plugin.Readme, which answers only for an
npm package name ("%q isn't an npm package name"), so opening one said "npm
didn't answer" and offered an npm link for a folder that npm has never heard
of.
The field now has the system's folder picker beside it, and a folder plugin's
page shows the README that folder carries.
- POST /api/plugins/choose asks Windows.ChooseFolder, which the app answers
with the system's panel — the Library's "Choose a project" already used it,
and a page served by another magpie forwards it to the magpie it is served
by. pluginsState carries "picker" beside "bun", false for `magpie web`: a
browser has no such panel (webHost.ChooseFolder refuses it), and the page is
then left with the field to type into, as before.
- plugin.Readme takes a path: folderReadme reads README.md whatever its case,
else a markdown or a text one named beside it, and says so when the folder
has none rather than answering with npm's silence. Updated is that file's
mtime, and the text is capped at 200 KB as npm's answer is.
- A folder plugin's page drops what only npm can say: no npm link, no version,
downloads or license, and a failure names the folder rather than npm. Its
row and its title name the folder it was added from (opencode-mine, not
/Users/me/plugins/opencode-mine).
TestReadmeFromFolder (internal/plugin) fails without the change: with a
README.txt and an unsuffixed README beside it, the README.md is still the one
read; ReadMe.MD is too, and through file:// and a trailing .; a folder with
none is an error, as is one that isn't there.
TestPluginsPickerWhereItCan (internal/gui) fails without the change: the
picker is offered for the app, not for `magpie web` nor for no host at all.
plugin-local.test.cjs (new; Chromium and WebKit, en and zh) fails without the
change: the picker is beside the field and titled as such, a click asks for it
and what it answers is what the field holds and takes the focus, what the
field holds is what Install posts, the Installed row names the folder rather
than its path, its page renders that folder's README with no npm link, and a
market that offers no picker draws no button (english and Chinese).
Not only the faked page: a sandbox `magpie web` in a HOME of its own, with a
plugin added by path, read that folder's README.md — its mtime as Updated —
and listed it by the folder's name.
go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows
go build, go test -tags nogui ./internal/gui/ pass; plugin-market,
plugin-signin and plugin-local pass in Chromium and WebKit, en and zh.
TestPluginProxy (internal/plugin) and the hermes tests (internal/agent,
internal/profile) fail on main on this machine too — its own proxy and a
hermes install it finds — and are unrelated.
Not done: the panel itself hasn't been clicked in the app. It is the system's
own, and the call under it is the one the Library's folder chooser already
uses, but the screenshots here are the page with a faked backend underneath
(the app's own picker is offered, `magpie web` has none), plus the real
`magpie web` for the README.
218 lines
15 KiB
CSS
218 lines
15 KiB
CSS
/* Plugins: the market of provider plugins, and the ones installed. */
|
|
#view-plugins { gap: 0; }
|
|
.pm-head { gap: 10px; }
|
|
.pm-find {
|
|
display: flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; width: min(280px, 46%);
|
|
border-radius: 8px; background: var(--pill); color: var(--muted);
|
|
box-shadow: inset 0 0 0 1px transparent; transition: box-shadow .14s var(--ease), background .14s var(--ease);
|
|
}
|
|
.pm-find:focus-within { background: var(--card); box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
|
|
.pm-find svg { flex: none; }
|
|
.pm-find input { flex: 1; min-width: 0; border: 0; outline: none; background: none; color: var(--fg); font: inherit; font-size: 12.5px; }
|
|
.pm-find input::placeholder { color: var(--faint); }
|
|
.pm-find input::-webkit-search-cancel-button { cursor: default; }
|
|
.pm-body { gap: 18px; padding: 10px 0 18px; }
|
|
|
|
/* the opening: what plugins are, and a few of the subscriptions they bring */
|
|
.pm-intro {
|
|
position: relative; overflow: hidden; display: flex; align-items: center; gap: 18px;
|
|
padding: 16px 18px; border-radius: 14px; border: 1px solid var(--line);
|
|
background:
|
|
radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--accent) 11%, transparent), transparent 60%),
|
|
var(--card);
|
|
}
|
|
.pm-introtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
.pm-intro h2 { margin: 0; font-size: 15px; font-weight: 650; letter-spacing: -.01em; text-wrap: balance; }
|
|
.pm-intro p { margin: 0; font-size: 12px; line-height: 1.55; color: var(--fg-2); max-width: 560px; }
|
|
.pm-intro .pm-trust { display: flex; align-items: center; gap: 6px; margin-top: 3px; font-size: 11.5px; color: var(--muted); }
|
|
.pm-intro .pm-trust svg { flex: none; color: var(--green); }
|
|
.pm-art { flex: none; display: grid; grid-template-columns: repeat(3, 34px); gap: 8px; transform: rotate(-6deg); margin-right: 6px; }
|
|
.pm-art .pm-logo { width: 34px; height: 34px; border-radius: 10px; box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 0 0 1px var(--line-2); background: var(--card); }
|
|
.pm-art .pm-logo:nth-child(4) { grid-column: 2; }
|
|
.pm-art .pm-logo:nth-child(odd) { transform: translateY(4px); }
|
|
|
|
/* sections of cards */
|
|
.pm-sec { display: flex; flex-direction: column; gap: 9px; }
|
|
.pm-sechead { display: flex; align-items: baseline; gap: 8px; padding: 0 2px; flex-wrap: wrap; }
|
|
.pm-sechead h3 { margin: 0; font-size: 12.5px; font-weight: 650; }
|
|
.pm-sechead span { font-size: 11.5px; color: var(--muted); }
|
|
.pm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(262px, 1fr)); gap: 10px; }
|
|
|
|
.pm-logo { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; background: var(--pill); color: var(--fg-2); }
|
|
.pm-logo .ic { width: 20px; height: 20px; }
|
|
.pm-logo.big { width: 44px; height: 44px; border-radius: 12px; }
|
|
.pm-logo.big .ic { width: 25px; height: 25px; }
|
|
|
|
.pm-card {
|
|
display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 12px 12px 11px 13px;
|
|
border-radius: 12px; border: 1px solid var(--line); background: var(--card); cursor: default; outline: none;
|
|
transition: border-color .16s var(--ease), box-shadow .16s var(--ease), transform .16s var(--ease);
|
|
}
|
|
.pm-card:hover { border-color: color-mix(in srgb, var(--fg) 16%, var(--line)); box-shadow: 0 4px 14px rgba(20, 22, 30, .07); }
|
|
.pm-card:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent); border-color: var(--accent); }
|
|
.pm-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
.pm-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
.pm-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
.pm-name b { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.pm-by { display: flex; align-items: center; gap: 4px; min-width: 0; font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.pm-by > span { overflow: hidden; text-overflow: ellipsis; }
|
|
.pm-verified { display: inline-flex; align-items: center; gap: 3px; color: var(--green); font-weight: 500; }
|
|
.pm-sum {
|
|
margin: 0; font-size: 12px; line-height: 1.45; color: var(--fg-2); min-height: 2.9em;
|
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
}
|
|
.pm-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; min-height: 18px; }
|
|
.pm-dl { display: inline-flex; align-items: center; gap: 3px; }
|
|
.pm-ver { font: 10.5px var(--code); color: var(--muted); }
|
|
.pm-chip { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 5px; font-size: 10.5px; font-weight: 500; background: var(--pill); color: var(--fg-2); }
|
|
.pm-chip.up { background: var(--accent-soft); color: var(--accent); }
|
|
.pm-chip.bad { background: var(--red-soft); color: var(--red); }
|
|
.pm-chip.moved { gap: 4px; background: var(--accent-soft); color: var(--accent); }
|
|
.pm-chip.moved .dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
|
.pm-row .sub.ask { color: var(--fg); }
|
|
.pm-chip.soft { background: none; box-shadow: inset 0 0 0 1px var(--line); color: var(--muted); }
|
|
|
|
/* a plugin's one button */
|
|
.pm-act {
|
|
flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
|
height: 26px; min-width: 64px; padding: 0 12px; border-radius: 13px;
|
|
font-size: 12px; font-weight: 600; white-space: nowrap;
|
|
background: var(--pill); color: var(--fg-2); transition: background .14s var(--ease), color .14s var(--ease), filter .14s;
|
|
}
|
|
.pm-act:hover:not(:disabled) { background: var(--pill-hover); color: var(--fg); }
|
|
.pm-act.get { background: var(--accent-soft); color: var(--accent); }
|
|
.pm-act.get:hover:not(:disabled) { background: var(--accent); color: var(--accent-fg); }
|
|
.pm-act.go { background: var(--accent); color: var(--accent-fg); }
|
|
.pm-act.go:hover:not(:disabled) { filter: brightness(1.08); background: var(--accent); color: var(--accent-fg); }
|
|
.pm-act.done { background: none; color: var(--green); padding: 0 8px; min-width: 0; }
|
|
.pm-act.done:hover:not(:disabled) { background: var(--green-soft); color: var(--green); }
|
|
.pm-act.busy { color: var(--muted); font-weight: 500; }
|
|
.pm-act:disabled:not(.busy):not(.done) { opacity: .55; }
|
|
.pm-card > .pm-act.move { height: auto; min-height: 26px; padding: 4px 12px; white-space: normal; text-align: center; }
|
|
.pm-act .spin { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid currentColor; border-right-color: transparent; animation: spin .7s linear infinite; }
|
|
|
|
/* loading: cards drawn as grey bars */
|
|
.pm-card.ghost { gap: 10px; pointer-events: none; }
|
|
.pm-card.ghost > div { border-radius: 6px; background: var(--pill); animation: pm-pulse 1.3s ease-in-out infinite; }
|
|
.pm-card.ghost .g1 { height: 36px; width: 60%; }
|
|
.pm-card.ghost .g2 { height: 11px; }
|
|
.pm-card.ghost .g3 { height: 11px; width: 40%; }
|
|
@keyframes pm-pulse { 50% { opacity: .5; } }
|
|
|
|
.pm-note { margin: 0; padding: 4px 2px; font-size: 12px; color: var(--muted); }
|
|
.pm-note.bad { color: var(--red); }
|
|
.pm-empty { margin: 24px 4px; font-size: 12.5px; color: var(--red); }
|
|
|
|
/* a package the market doesn't list */
|
|
.pm-manual { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 12px; border: 1px dashed var(--line); flex-wrap: wrap; }
|
|
.pm-mlabel { font-size: 12px; color: var(--muted); }
|
|
.pm-minput { flex: 1; min-width: 180px; height: 26px; padding: 0 9px; border-radius: 7px; border: 1px solid var(--line); background: var(--card); color: var(--fg); font: 12px var(--code); outline: none; }
|
|
.pm-minput:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
|
|
/* the system's folder picker, beside the field it fills */
|
|
.pm-browse { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; flex: none; padding: 0; border-radius: 7px; border: 1px solid var(--line); background: var(--card); color: var(--muted); cursor: pointer; transition: color .14s var(--ease), border-color .14s var(--ease); }
|
|
.pm-browse:hover:not(:disabled) { color: var(--fg); border-color: var(--faint); }
|
|
.pm-browse:disabled { opacity: .5; cursor: default; }
|
|
|
|
/* Installed */
|
|
.pm-list .row { min-height: 54px; padding: 8px 10px 8px 12px; gap: 11px; cursor: default; transition: background .14s var(--ease); }
|
|
.pm-list .row:hover { background: var(--card-2); }
|
|
.pm-list .row + .row { border-top: 1px solid var(--line-2); }
|
|
.pm-row .pm-logo { width: 32px; height: 32px; border-radius: 9px; }
|
|
.pm-row .pm-logo .ic { width: 18px; height: 18px; }
|
|
.pm-row .who { flex: 1; min-width: 0; }
|
|
.pm-row .name { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500; }
|
|
.pm-row .sub { display: flex; align-items: center; flex-wrap: wrap; gap: 0 2px; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
|
|
.pm-row .sub.bad { color: var(--red); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.pm-row .sub .sep { color: var(--faint); }
|
|
.pm-sub { display: inline-flex; align-items: center; gap: 5px; }
|
|
.pm-sub .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--faint); }
|
|
.pm-sub.in .dot { background: var(--green); }
|
|
.pm-row .val { display: flex; align-items: center; gap: 2px; flex: none; }
|
|
.pm-row.off .who, .pm-row.off .pm-logo { opacity: .5; }
|
|
.pm-foot { display: flex; align-items: center; gap: 8px; padding: 0 4px; font-size: 11px; color: var(--faint); }
|
|
.pm-none { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 44px 16px; text-align: center; }
|
|
.pm-none b { font-size: 13.5px; font-weight: 600; margin-top: 4px; }
|
|
.pm-none p { margin: 0 0 6px; font-size: 12px; color: var(--muted); max-width: 320px; line-height: 1.5; }
|
|
|
|
/* a plugin's page */
|
|
.modal .editor.pm-detail.framed > .ehead { min-height: 76px; gap: 12px; }
|
|
.modal .editor.pm-detail.framed > .ebody { display: block; padding: 0; }
|
|
.pm-dhead .pm-name b { font-size: 15px; }
|
|
.pm-dhead .pm-by { font-size: 11.5px; }
|
|
.pm-dact .pm-act { height: 28px; min-width: 80px; border-radius: 14px; }
|
|
.pm-dbody { display: flex; flex-direction: column; gap: 12px; padding: 14px 18px 18px; }
|
|
.pm-dsum { margin: 0; font-size: 13px; line-height: 1.5; color: var(--fg); }
|
|
.pm-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 1px; border-radius: 10px; overflow: hidden; background: var(--line-2); border: 1px solid var(--line-2); }
|
|
.pm-fact { display: flex; flex-direction: column; gap: 2px; padding: 8px 11px; background: var(--card); min-width: 0; }
|
|
.pm-fact .k { font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
.pm-fact .v { font-size: 12.5px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
.pm-links { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
.pm-link { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 9px; border-radius: 7px; background: var(--pill); color: var(--fg-2); font-size: 11.5px; text-decoration: none; }
|
|
.pm-link:hover { background: var(--pill-hover); color: var(--fg); }
|
|
.pm-readme { border-top: 1px solid var(--line-2); padding-top: 12px; min-height: 60px; }
|
|
.pm-rskel { height: 11px; margin: 8px 0; border-radius: 5px; background: var(--pill); animation: pm-pulse 1.3s ease-in-out infinite; }
|
|
.pm-rskel.short { width: 55%; }
|
|
|
|
/* a README */
|
|
.pm-md { font-size: 12.5px; line-height: 1.6; color: var(--fg-2); overflow-wrap: anywhere; }
|
|
.pm-md > :first-child { margin-top: 0; }
|
|
.pm-md h3, .pm-md h4, .pm-md h5, .pm-md h6 { color: var(--fg); margin: 16px 0 6px; line-height: 1.3; text-wrap: balance; }
|
|
.pm-md h3 { font-size: 15px; font-weight: 650; }
|
|
.pm-md h4 { font-size: 13.5px; font-weight: 650; }
|
|
.pm-md h5, .pm-md h6 { font-size: 12.5px; font-weight: 600; }
|
|
.pm-md p { margin: 0 0 9px; }
|
|
.pm-md ul, .pm-md ol { margin: 0 0 9px; padding-left: 20px; }
|
|
.pm-md li + li { margin-top: 2px; }
|
|
.pm-md a { color: var(--accent); text-decoration: none; }
|
|
.pm-md a:hover { text-decoration: underline; }
|
|
.pm-md code { font: 11.5px var(--code); padding: 1px 4px; border-radius: 4px; background: var(--pill); color: var(--fg); }
|
|
.pm-md pre { margin: 0 0 10px; padding: 9px 11px; border-radius: 8px; background: var(--card-2); border: 1px solid var(--line-2); overflow-x: auto; }
|
|
.pm-md pre code { padding: 0; background: none; font-size: 11.5px; line-height: 1.35; white-space: pre; } /* close enough for a box drawn in │ and ─ to join */
|
|
.pm-md blockquote { margin: 0 0 9px; padding: 7px 11px; border-radius: 8px; background: var(--card-2); color: var(--fg-2); }
|
|
.pm-md hr { border: 0; border-top: 1px solid var(--line-2); margin: 12px 0; }
|
|
.pm-table { overflow-x: auto; margin: 0 0 10px; }
|
|
.pm-md table { border-collapse: collapse; font-size: 12px; }
|
|
.pm-md th, .pm-md td { padding: 5px 9px; border: 1px solid var(--line-2); text-align: left; vertical-align: top; }
|
|
.pm-md th { background: var(--card-2); font-weight: 600; color: var(--fg); }
|
|
|
|
/* the providers list's way here: three subscriptions in a stack, and a word */
|
|
#view-providers .after-list { justify-content: space-between; }
|
|
.more-subs {
|
|
display: inline-flex; align-items: center; gap: 8px; height: 28px; padding: 0 9px 0 5px; border-radius: 14px;
|
|
background: var(--card); color: var(--fg-2); font-size: 12px;
|
|
box-shadow: 0 0 0 1px var(--line), 0 1px 2px rgba(0, 0, 0, .04);
|
|
transition: box-shadow .16s var(--ease), color .16s var(--ease);
|
|
}
|
|
.more-subs:hover { color: var(--fg); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, var(--line)), 0 3px 10px rgba(20, 22, 30, .08); }
|
|
.more-subs .stack { display: inline-flex; align-items: center; }
|
|
.more-subs .stack .ic {
|
|
width: 20px; height: 20px; padding: 3px; box-sizing: border-box; border-radius: 50%;
|
|
background: var(--card); box-shadow: 0 0 0 1px var(--line-2); transition: transform .22s var(--ease);
|
|
}
|
|
.more-subs .stack .ic + .ic { margin-left: -6px; }
|
|
.more-subs .stack .ic:nth-child(1) { z-index: 3; position: relative; }
|
|
.more-subs .stack .ic:nth-child(2) { z-index: 2; position: relative; }
|
|
.more-subs:hover .stack .ic:nth-child(2) { transform: translateX(2px); }
|
|
.more-subs:hover .stack .ic:nth-child(3) { transform: translateX(4px); }
|
|
.more-subs .say { display: inline-flex; gap: 4px; white-space: nowrap; }
|
|
.more-subs .say .in { color: var(--accent); font-weight: 500; }
|
|
.more-subs > svg { color: var(--faint); transition: transform .18s var(--ease), color .18s; }
|
|
.more-subs:hover > svg { color: var(--accent); transform: translateX(2px); }
|
|
|
|
/* the add sheet's row to Plugins */
|
|
.tile.more-plugins { color: var(--accent); }
|
|
.tile.more-plugins .puzzle { flex: none; width: 18px; height: 18px; display: grid; place-items: center; }
|
|
.tile.more-plugins > svg { flex: none; opacity: .7; }
|
|
.tile.more-plugins:hover { background: var(--accent-soft); }
|
|
|
|
@media (max-width: 560px) {
|
|
.pm-head { flex-wrap: wrap; }
|
|
.pm-find { width: 100%; order: 3; }
|
|
.pm-art { display: none; }
|
|
.more-subs .say .in { display: none; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.pm-card, .more-subs .stack .ic, .more-subs > svg { transition: none; }
|
|
.pm-card.ghost > div, .pm-rskel { animation: none; }
|
|
}
|