@gemboss/ui 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/README.md +129 -1
- package/catalog/editor.html +1 -1
- package/catalog/index.html +229 -86
- package/gemboss-ui.css +2 -2
- package/package.json +10 -2
- package/parts/components.css +1 -1
- package/parts/core.css +1 -1
- package/parts/shell.css +1 -1
- package/src/assistant-plain.js +1 -1
- package/src/editor/layers.tsx +1 -1
- package/src/editor.css +1 -1
- package/src/icons.tsx +2 -0
- package/src/index.ts +8 -1
- package/src/language-switcher-plain.js +1 -0
- package/src/language-switcher.css +1 -0
- package/src/language-switcher.tsx +206 -0
- package/src/logo-sheet.css +1 -0
- package/src/logo-sheet.tsx +63 -0
- package/src/question-card.css +1 -1
- package/src/question-card.tsx +19 -4
- package/src/session-rail-plain.js +1 -0
- package/src/setup-guide.css +1 -1
- package/src/setup-guide.tsx +92 -22
package/src/question-card.tsx
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Presentational and controlled: no state of its own, so it renders on the server. The choices are real radio inputs (a keyboard
|
|
18
18
|
* and a screen reader get a radio group, the arrow keys move the selection); the dot and the highlight are drawn by the card.
|
|
19
|
+
*
|
|
20
|
+
* A choice made by LOOKING (unreleased, after 0.46.0; the store's logo, GemBoss Brand): `media` draws a picture above the choice's
|
|
21
|
+
* words, `layout="tiles"` puts the choices side by side (one column on a phone), and `disabled` shows a choice that cannot be taken
|
|
22
|
+
* (an option that failed its checks: seen, greyed, its radio off, and the confirm stays off while it is the value).
|
|
19
23
|
* Its stylesheet travels with it (`question-card.css`, own entry; `@gemboss/ui/question-card.css` for a plain-HTML surface that
|
|
20
24
|
* writes the same classes).
|
|
21
25
|
*/
|
|
@@ -35,6 +39,12 @@ export interface QuestionChoice {
|
|
|
35
39
|
note?: ReactNode;
|
|
36
40
|
/** The assistant's best guess: gets the pick tag. At most one choice should carry it, and it goes first. */
|
|
37
41
|
pick?: boolean;
|
|
42
|
+
/** A picture of the choice, above its words: for a choice a person makes by looking (a logo, a layout). Phrasing content only
|
|
43
|
+
* (spans, images): the choice is a `<label>`. */
|
|
44
|
+
media?: ReactNode;
|
|
45
|
+
/** Shown but not choosable (an option that failed): its radio is disabled, the row greyed, and the confirm stays off while it is
|
|
46
|
+
* the value. A surface should not make a disabled choice the `pick` or the starting `value`. */
|
|
47
|
+
disabled?: boolean;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/** One, two or three choices. A fourth does not type-check. */
|
|
@@ -71,17 +81,21 @@ export interface QuestionCardProps {
|
|
|
71
81
|
fine?: ReactNode;
|
|
72
82
|
/** While the confirm is being saved: the card stops taking input. */
|
|
73
83
|
busy?: boolean;
|
|
84
|
+
/** `rows` (default): one choice under the other. `tiles`: side by side, each with its `media` on top; one column on a phone. */
|
|
85
|
+
layout?: "rows" | "tiles";
|
|
74
86
|
className?: string;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
export function QuestionCard({
|
|
78
90
|
id, question, eyebrow, evidence, art, choices, value, onChange, confirmLabel, onConfirm, pickLabel = "Gem's pick",
|
|
79
|
-
onOther, otherLabel = "Type my own", onSkip, skipLabel = "Skip", fine, busy, className,
|
|
91
|
+
onOther, otherLabel = "Type my own", onSkip, skipLabel = "Skip", fine, busy, layout = "rows", className,
|
|
80
92
|
}: QuestionCardProps) {
|
|
81
93
|
const list: readonly QuestionChoice[] = choices.slice(0, MAX_QUESTION_CHOICES);
|
|
82
94
|
const qid = `${id}-q`;
|
|
95
|
+
// The confirm commits the value only when it is a choice that can be taken: never a disabled one, never one that is not listed.
|
|
96
|
+
const confirmable = list.some((c) => c.id === value && !c.disabled);
|
|
83
97
|
return (
|
|
84
|
-
<section className={cx("ui-qcard", className)} aria-labelledby={qid} aria-busy={busy || undefined}>
|
|
98
|
+
<section className={cx("ui-qcard", layout === "tiles" && "ui-qcard--tiles", className)} aria-labelledby={qid} aria-busy={busy || undefined}>
|
|
85
99
|
<header className="ui-qcard__head">
|
|
86
100
|
{art ? <div className="ui-qcard__art" aria-hidden>{art}</div> : null}
|
|
87
101
|
<div className="ui-qcard__ask">
|
|
@@ -93,8 +107,9 @@ export function QuestionCard({
|
|
|
93
107
|
<div className="ui-qcard__choices" role="radiogroup" aria-labelledby={qid}>
|
|
94
108
|
{list.map((c) => (
|
|
95
109
|
<label className="ui-qcard__choice" key={c.id}>
|
|
96
|
-
<input className="ui-qcard__input" type="radio" name={id} value={c.id} checked={value === c.id} disabled={busy}
|
|
110
|
+
<input className="ui-qcard__input" type="radio" name={id} value={c.id} checked={value === c.id} disabled={busy || c.disabled}
|
|
97
111
|
onChange={() => onChange(c.id)} />
|
|
112
|
+
{c.media ? <span className="ui-qcard__media">{c.media}</span> : null}
|
|
98
113
|
<span className="ui-qcard__dot" aria-hidden />
|
|
99
114
|
<span className="ui-qcard__text">
|
|
100
115
|
<span className="ui-qcard__label">{c.label}</span>
|
|
@@ -105,7 +120,7 @@ export function QuestionCard({
|
|
|
105
120
|
))}
|
|
106
121
|
</div>
|
|
107
122
|
<div className="ui-qcard__foot">
|
|
108
|
-
<button type="button" className="ui-btn ui-btn--primary ui-qcard__confirm" disabled={busy || !
|
|
123
|
+
<button type="button" className="ui-btn ui-btn--primary ui-qcard__confirm" disabled={busy || !confirmable}
|
|
109
124
|
onClick={() => onConfirm(value)}>{confirmLabel}</button>
|
|
110
125
|
{onOther ? <button type="button" className="ui-btn ui-btn--ghost" disabled={busy} onClick={onOther}>{otherLabel}</button> : null}
|
|
111
126
|
{onSkip ? <button type="button" className="ui-btn ui-btn--ghost" disabled={busy} onClick={onSkip}>{skipLabel}</button> : null}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(g){"use strict";var q=g.gembossUI;if(!q){g.console&&g.console.error("@gemboss/ui session-rail-plain.js: load kit-plain.js first (it defines gembossUI)");return}if(q.sessionRail)return;var S="http://www.w3.org/2000/svg",j=function(r){return typeof Node<"u"&&r instanceof Node};function B(r,t){var i={},n;for(n in r)i[n]=r[n];for(n in t)i[n]=t[n];return i}function l(r,t,i){var n=document.createElement(r);if(t)for(var o in t){var d=t[o];d==null||d===!1||(o==="className"?n.className=d:n.setAttribute(o,d===!0?"":String(d)))}return(Array.isArray(i)?i:i==null?[]:[i]).forEach(function(a){n.appendChild(j(a)?a:document.createTextNode(String(a)))}),n}function Q(r){try{return g.localStorage.getItem(r)}catch{return null}}function J(r,t){try{g.localStorage.setItem(r,t)}catch{}}var O={newChat:"New chat",expand:"Expand sidebar",collapse:"Collapse sidebar",expandTitle:"Expand",collapseTitle:"Collapse",searchPlaceholder:"Search conversations\u2026",searchLabel:"Search history",clearSearch:"Clear search",noneFound:"No conversations found",noneYet:"No conversations yet",openNow:"Open now",pinned:"Pinned",today:"Today",yesterday:"Yesterday",previous7:"Previous 7 days",previous30:"Previous 30 days",older:"Older",working:"working",options:"Options",save:"Save",cancel:"Cancel",pin:"Pin",unpin:"Unpin",rename:"Rename",exportMarkdown:"Export Markdown",remove:"Delete",confirmRemove:"Delete this chat?",justNow:"just now",minutesAgo:function(r){return r+" min ago"},hoursAgo:function(r){return r+" hr ago"},daysAgo:function(r){return r+" day"+(r>1?"s":"")+" ago"},dateLocale:"en-US"};function H(r,t,i){t=t||O;var n=Math.floor(((i===void 0?Date.now():i)-r)/6e4);if(n<1)return t.justNow;if(n<60)return t.minutesAgo(n);var o=Math.floor(n/60);if(o<24)return t.hoursAgo(o);var d=Math.floor(o/24);return d<7?t.daysAgo(d):new Date(r).toLocaleDateString(t.dateLocale)}function P(r){return String(r).toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/đ/g,"d")}function _(r,t,i){t=t||O;var n=r.filter(function(p){return p.pinned}),o=r.filter(function(p){return!p.pinned}),d=new Date(i===void 0?Date.now():i),a=new Date(d.getFullYear(),d.getMonth(),d.getDate()).getTime(),A=864e5,T=[{label:t.today,min:a},{label:t.yesterday,min:a-A},{label:t.previous7,min:a-7*A},{label:t.previous30,min:a-30*A},{label:t.older,min:-1/0}],f=T.map(function(p){return{label:p.label,items:[]}});o.forEach(function(p){for(var v=-1,L=0;L<T.length;L++)if(p.updatedAt>=T[L].min){v=L;break}f[v===-1?f.length-1:v].items.push(p)});var w=n.length?[{label:t.pinned,items:n}]:[];return w.concat(f.filter(function(p){return p.items.length>0}))}q.assistantSessions={relativeTime:H,normalizeForSearch:P,groupSessions:_,labels:O};function b(r){return function(t){var i=document.createElementNS(S,"svg");i.setAttribute("viewBox","0 0 20 20"),t&&i.setAttribute("class",t),i.setAttribute("aria-hidden","true"),i.setAttribute("focusable","false");var n=document.createElementNS(S,"g");return[["fill","none"],["stroke","currentColor"],["stroke-width","1.6"],["stroke-linecap","round"],["stroke-linejoin","round"]].forEach(function(o){n.setAttribute(o[0],o[1])}),r.forEach(function(o){var d=document.createElementNS(S,o.tag||"path");for(var a in o)a!=="tag"&&d.setAttribute(a,o[a]);n.appendChild(d)}),i.appendChild(n),i}}var X={compose:b([{d:"M11 4H5.5A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16h9a1.5 1.5 0 0 0 1.5-1.5V9"},{d:"M14.5 3.5l2 2L10 12H8v-2z"}]),chevronLeft:b([{d:"M12 5l-5 5 5 5"}]),chevronRight:b([{d:"M8 5l5 5-5 5"}]),search:b([{tag:"circle",cx:"9",cy:"9",r:"5"},{d:"M13 13l3.5 3.5"}]),close:b([{d:"M6 6l8 8M14 6l-8 8"}]),check:b([{d:"M4.5 10.5l3.5 3.5 7.5-8"}]),more:b([{tag:"circle",cx:"5",cy:"10",r:".8"},{tag:"circle",cx:"10",cy:"10",r:".8"},{tag:"circle",cx:"15",cy:"10",r:".8"}]),pin:b([{d:"M8 3.5h4l-.5 5 2.5 2.5H6l2.5-2.5z"},{d:"M10 11v5.5"}]),pinFilled:function(r){var t=document.createElementNS(S,"svg");t.setAttribute("viewBox","0 0 20 20"),r&&t.setAttribute("class",r),t.setAttribute("aria-hidden","true"),t.setAttribute("focusable","false");var i=document.createElementNS(S,"path");i.setAttribute("d","M8 3.5h4l-.5 5 2.5 2.5H6l2.5-2.5z"),i.setAttribute("fill","currentColor");var n=document.createElementNS(S,"path");return n.setAttribute("d","M10 11v5.5"),n.setAttribute("stroke","currentColor"),n.setAttribute("stroke-width","1.6"),n.setAttribute("stroke-linecap","round"),t.appendChild(i),t.appendChild(n),t},edit:b([{d:"M4 16h3l8.5-8.5-3-3L4 13z"}]),exportIcon:b([{d:"M10 3.5v9M6.5 7L10 3.5 13.5 7"},{d:"M4 12.5v3h12v-3"}]),remove:b([{d:"M5 6h10M8 6V4.5h4V6M6.5 6l.7 9.5h5.6l.7-9.5"}])};q.sessionRail=function(r){r=r||{};var t={sessions:[],unlistedIds:[],activeId:void 0,pendingId:null,busyIds:[],collapsed:!1,footer:null,titleMaxLength:200,newChatTitle:void 0},i={},n=O,o=X,d=r.storageKey||"",a={query:"",editingId:null,editValue:"",menuId:null,menuTop:0,menuLeft:0,confirmDeleteId:null},A=[];function T(e){["sessions","unlistedIds","busyIds"].forEach(function(s){e[s]!==void 0&&(t[s]=e[s]||[])}),["activeId","pendingId","footer","newChatTitle"].forEach(function(s){e[s]!==void 0&&(t[s]=e[s])}),e.collapsed!==void 0&&(t.collapsed=!!e.collapsed),e.titleMaxLength!==void 0&&(t.titleMaxLength=e.titleMaxLength||200),e.labels&&(n=B(n,e.labels)),e.icons&&(o=B(o,e.icons)),["onToggleCollapsed","onNewChat","onOpen","onRename","onTogglePin","onDelete","onExport"].forEach(function(s){e[s]!==void 0&&(i[s]=e[s])})}T(r),r.collapsed===void 0&&d&&(t.collapsed=Q(d)==="1");var f=l("aside",{className:"gemboss-assistant-rail"}),w=l("button",{type:"button",className:"gemboss-assistant-newchat"});w.addEventListener("click",function(){i.onNewChat&&i.onNewChat()});var p=l("button",{type:"button",className:"gemboss-assistant-collapse"});p.addEventListener("click",function(){if(i.onToggleCollapsed){i.onToggleCollapsed();return}t.collapsed=!t.collapsed,d&&J(d,t.collapsed?"1":"0"),K()}),f.appendChild(l("div",{className:"gemboss-assistant-rail-top"},[w,p]));var v=l("div",{className:"gemboss-assistant-search gemboss-field gemboss-field--group"}),L=o.search(),y=l("input",{className:"gemboss-field__bare"}),V=null;y.addEventListener("input",function(){a.query=y.value,F(),E()}),y.addEventListener("keydown",function(e){e.key==="Escape"&&(e.preventDefault(),Y(""),y.blur())}),v.appendChild(L),v.appendChild(y);var C=l("div",{className:"gemboss-assistant-threads"});f.appendChild(C);var z=[];function Y(e){a.query=e,y.value=e,F(),E()}function F(){var e;a.query?(e=l("button",{type:"button",className:"gemboss-assistant-search-clear","aria-label":n.clearSearch},o.close()),e.addEventListener("click",function(){Y("")})):e=l("kbd",{className:"gemboss-assistant-kbd"},"/"),V?v.replaceChild(e,V):v.appendChild(e),V=e}function M(){a.menuId!==null&&(a.menuId=null,U(),E())}function U(){A.forEach(function(e){e()}),A=[]}function Z(){U();var e=function(u){var m=C.querySelector(".gemboss-assistant-menu");(!m||!m.contains(u.target))&&M()},s=function(){M()};document.addEventListener("mousedown",e),g.addEventListener("scroll",s,!0),g.addEventListener("resize",s),A.push(function(){document.removeEventListener("mousedown",e),g.removeEventListener("scroll",s,!0),g.removeEventListener("resize",s)})}function $(e,s){if(a.menuId===s){M();return}var u=e.getBoundingClientRect(),m=184,c=200,h=u.bottom+c+8>g.innerHeight;a.menuLeft=Math.max(8,Math.min(u.right-m,g.innerWidth-m-8)),a.menuTop=h?Math.max(8,u.top-6-c):u.bottom+6,a.confirmDeleteId=null,a.menuId=s,Z(),E()}function ee(e){a.editingId=e.id,a.editValue=e.title,E()}function W(){a.editingId=null,a.editValue="",E()}function G(e){var s=a.editValue.trim();a.editingId=null,E(),!(!s||s===e.title)&&i.onRename&&i.onRename(e.id,s)}function D(e,s,u,m){var c=l("button",{type:"button",role:"menuitem",className:m?"danger":null},[e,l("span",null,s)]);return c.addEventListener("click",u),c}function te(e){var s=a.editingId===e.id,u=l("div",{className:"gemboss-assistant-thread"+(e.id===t.activeId||e.id===t.pendingId?" active":"")+(s?" editing":"")+(a.menuId===e.id?" menu-open":"")});if(s){var m=l("input",{className:"gemboss-field",maxlength:String(t.titleMaxLength)});m.value=a.editValue,m.addEventListener("input",function(){a.editValue=m.value}),m.addEventListener("keydown",function(x){x.key==="Enter"?(x.preventDefault(),G(e)):x.key==="Escape"&&(x.preventDefault(),W())});var c=l("button",{type:"button",className:"gemboss-assistant-thread-act","aria-label":n.save},o.check());c.addEventListener("click",function(){G(e)});var h=l("button",{type:"button",className:"gemboss-assistant-thread-act","aria-label":n.cancel},o.close());return h.addEventListener("click",W),u.appendChild(l("div",{className:"gemboss-assistant-thread-edit"},[m,c,h])),u}var I=l("span",{className:"gemboss-assistant-thread-titlerow"});t.busyIds.indexOf(e.id)>=0&&I.appendChild(l("span",{className:"gemboss-assistant-thread-running","aria-label":n.working})),e.pinned&&I.appendChild(o.pinFilled("gemboss-assistant-pin-dot")),I.appendChild(l("span",{className:"gemboss-assistant-thread-title"},e.title));var k=l("button",{type:"button",className:"gemboss-assistant-thread-main",title:e.title},[I,l("span",{className:"gemboss-assistant-thread-meta"},H(e.updatedAt,n))]);k.addEventListener("click",function(){i.onOpen&&i.onOpen(e.id)}),k.gbRailKey=e.id+":main";var R=l("button",{type:"button",className:"gemboss-assistant-thread-act","aria-label":n.options},o.more());if(R.addEventListener("mousedown",function(x){x.stopPropagation()}),R.addEventListener("click",function(){$(R,e.id)}),R.gbRailKey=e.id+":more",u.appendChild(k),u.appendChild(l("div",{className:"gemboss-assistant-thread-actions"},R)),a.menuId===e.id){var N=l("div",{className:"gemboss-popover-surface gemboss-assistant-menu",role:"menu"});N.style.position="fixed",N.style.top=a.menuTop+"px",N.style.left=a.menuLeft+"px",a.confirmDeleteId===e.id?(N.appendChild(l("div",{className:"gemboss-assistant-menu-confirm"},n.confirmRemove)),N.appendChild(D(o.remove(),n.remove,function(){M(),i.onDelete&&i.onDelete(e.id)},!0)),N.appendChild(D(o.close(),n.cancel,function(){a.confirmDeleteId=null,E()}))):(i.onTogglePin&&N.appendChild(D(e.pinned?o.pin():o.pinFilled(),e.pinned?n.unpin:n.pin,function(){M(),i.onTogglePin(e)})),N.appendChild(D(o.edit(),n.rename,function(){M(),ee(e)})),i.onExport&&N.appendChild(D(o.exportIcon(),n.exportMarkdown,function(){M(),i.onExport(e.id)})),N.appendChild(D(o.remove(),n.remove,function(){a.confirmDeleteId=e.id,E()},!0))),u.appendChild(N)}return u}function ne(){var e=P(a.query.trim());if(e){var s=t.sessions.filter(function(c){return P(c.title+" "+(c.preview||"")).indexOf(e)>=0});return s.length?[{label:"",items:s}]:[]}var u=_(t.sessions,n),m=t.unlistedIds.filter(function(c){return!t.sessions.some(function(h){return h.id===c})}).map(function(c){return{id:c,title:n.newChat,updatedAt:Date.now(),pinned:!1}});return m.length?[{label:n.openNow,items:m}].concat(u):u}function E(){for(var e=document.activeElement&&C.contains(document.activeElement)?document.activeElement:null,s=!!e&&e.tagName==="INPUT",u=e?e.gbRailKey:null;C.firstChild;)C.removeChild(C.firstChild);var m=ne();m.length?m.forEach(function(h){var I=l("div",{className:"gemboss-assistant-group"});h.label&&I.appendChild(l("div",{className:"gemboss-assistant-group-label"},h.label)),h.items.forEach(function(k){I.appendChild(te(k))}),C.appendChild(I)}):C.appendChild(l("div",{className:"gemboss-assistant-threads-empty"},a.query?n.noneFound:n.noneYet)),u&&Array.prototype.some.call(C.querySelectorAll("button"),function(h){return h.gbRailKey!==u?!1:(h.focus({preventScroll:!0}),!0)});var c=C.querySelector(".gemboss-assistant-thread-edit input");if(c&&(s||a.editingId!==null)){c.focus();try{c.setSelectionRange(c.value.length,c.value.length)}catch{}}}function K(){for(f.className="gemboss-assistant-rail"+(t.collapsed?" is-collapsed":"");w.firstChild;)w.removeChild(w.firstChild);for(w.title=t.newChatTitle!==void 0&&t.newChatTitle!==null?t.newChatTitle:n.newChat,w.appendChild(o.compose()),w.appendChild(l("span",null,n.newChat));p.firstChild;)p.removeChild(p.firstChild);p.setAttribute("aria-label",t.collapsed?n.expand:n.collapse),p.title=t.collapsed?n.expandTitle:n.collapseTitle,p.appendChild(t.collapsed?o.chevronRight():o.chevronLeft()),t.sessions.length>0?(y.setAttribute("placeholder",n.searchPlaceholder),y.setAttribute("aria-label",n.searchLabel),y.value!==a.query&&(y.value=a.query),F(),v.parentNode!==f&&f.insertBefore(v,C)):v.parentNode===f&&f.removeChild(v),E(),z.forEach(function(e){e.parentNode===f&&f.removeChild(e)}),z=t.footer?(Array.isArray(t.footer)?t.footer:[t.footer]).filter(j):[],z.forEach(function(e){f.appendChild(e)})}return K(),{el:f,update:function(e){T(e||{}),K()},focusSearch:function(){v.parentNode===f&&y.focus()},destroy:function(){U(),f.parentNode&&f.parentNode.removeChild(f)}}}})(typeof globalThis<"u"?globalThis:this);
|
package/src/setup-guide.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.ui-setup,.ui-setup__inner{display:grid;gap:var(--ui-space-3, 12px)}.ui-setup__head,.ui-setup__row{display:flex;align-items:center;gap:var(--ui-space-3, 12px);list-style:none;cursor:pointer;border-radius:var(--ui-radius-md, 8px);color:inherit;transition:background-color .12s ease}.ui-setup__head::-webkit-details-marker,.ui-setup__row::-webkit-details-marker{display:none}div.ui-setup__head{cursor:auto}summary.ui-setup__head{padding:6px 8px;margin:-6px -8px 0}.ui-setup__title{margin:0;font-size:var(--ui-fs-body, 13px);line-height:20px;font-weight:650;color:var(--ui-text-strong, var(--ui-text))}.ui-setup__desc{margin:0;font-size:var(--ui-fs-body, 13px);line-height:20px;color:var(--ui-text-muted)}.ui-setup__row{padding:8px;margin:0 -8px}summary.ui-setup__head:hover,summary.ui-setup__row:hover{background:var(--ui-surface-2, #eceef2)}.ui-setup__row--static{cursor:auto}.ui-setup__row.ui-setup__row--static:after{content:none}summary.ui-setup__head:active,summary.ui-setup__row:active{background:var(--ui-border, #e5e7ea)}summary.ui-setup__head:focus-visible,summary.ui-setup__row:focus-visible{outline:2px solid var(--ui-focus, #5661f5);outline-offset:2px}summary.ui-setup__head:after,.ui-setup__row:after{content:"\25be";margin-left:auto;font-size:11px;color:var(--ui-text-muted);transition:transform .12s ease}.ui-setup:not([open])>summary.ui-setup__head:after,.ui-
|
|
1
|
+
.ui-setup{--ui-setup-done: var(--gemboss-success, #007f5f)}.ui-setup,.ui-setup__inner{display:grid;gap:var(--ui-space-3, 12px)}.ui-setup__head,.ui-setup__row{display:flex;align-items:center;gap:var(--ui-space-3, 12px);list-style:none;cursor:pointer;border-radius:var(--ui-radius-md, 8px);color:inherit;transition:background-color .12s ease}.ui-setup__head::-webkit-details-marker,.ui-setup__row::-webkit-details-marker{display:none}div.ui-setup__head{cursor:auto}summary.ui-setup__head{padding:6px 8px;margin:-6px -8px 0}.ui-setup__title{margin:0;font-size:var(--ui-fs-body, 13px);line-height:20px;font-weight:650;color:var(--ui-text-strong, var(--ui-text))}.ui-setup .ui-badge--warning{background:var(--gemboss-warning-soft);color:var(--gemboss-warning)}.ui-setup .ui-badge--success{background:var(--gemboss-success-soft);color:var(--gemboss-success-strong)}.ui-setup__desc{margin:0;font-size:var(--ui-fs-body, 13px);line-height:20px;color:var(--ui-text-muted)}.ui-setup__row{padding:8px;margin:0 -8px}summary.ui-setup__head:hover,summary.ui-setup__row:hover,.ui-setup__x:hover{background:var(--ui-surface-2, #eceef2)}.ui-setup__row--static{cursor:auto}.ui-setup__row.ui-setup__row--static:after{content:none}summary.ui-setup__head:active,summary.ui-setup__row:active,.ui-setup__x:active{background:var(--ui-border, #e5e7ea)}summary.ui-setup__head:focus-visible,summary.ui-setup__row:focus-visible,.ui-setup__x:focus-visible{outline:2px solid var(--ui-focus, #5661f5);outline-offset:2px}summary.ui-setup__head:after,.ui-setup__row:after{content:"\25be";margin-left:auto;font-size:11px;color:var(--ui-text-muted);transition:transform .12s ease}.ui-setup:not([open])>summary.ui-setup__head:after,:is(.ui-setup__step,.ui-setup__done):not([open])>.ui-setup__row:after{transform:rotate(-90deg)}.ui-setup__bar{appearance:none;display:block;width:100%;height:6px;border:0;border-radius:999px;overflow:hidden;background:var(--ui-border, #e5e7ea);color:var(--ui-setup-done)}.ui-setup__bar::-webkit-progress-bar{background:inherit;border-radius:inherit}.ui-setup__bar::-webkit-progress-value{background:currentColor;border-radius:inherit;transition:width .15s ease}.ui-setup__bar::-moz-progress-bar{background:currentColor;border-radius:inherit}.ui-setup__steps{display:grid;gap:2px}.ui-setup__mark{flex:0 0 18px;width:18px;height:18px;box-sizing:border-box;display:inline-grid;place-items:center;position:relative;border:1.5px solid color-mix(in srgb,var(--ui-text-muted, #686b71) 50%,transparent);border-radius:50%;font-size:0;color:var(--ui-text-muted, #686b71)}:is(.ui-setup__step[data-state=done],.ui-setup__done)>.ui-setup__row>.ui-setup__mark{background:var(--ui-setup-done);border-color:var(--ui-setup-done)}:is(.ui-setup__step[data-state=done],.ui-setup__done)>.ui-setup__row>.ui-setup__mark:before{content:"";position:absolute;left:5px;top:2px;width:4px;height:8px;box-sizing:content-box;border:solid #fff;border-width:0 1.75px 1.75px 0;transform:rotate(45deg)}.ui-setup__step[data-state=unknown]>.ui-setup__row>.ui-setup__mark{border-style:dashed;font-size:10px;font-weight:650}.ui-setup__label{min-width:0;overflow-wrap:anywhere;font-size:var(--ui-fs-body, 13px);line-height:20px;font-weight:550;color:var(--ui-text)}:is(.ui-setup__step[data-state=done],.ui-setup__done)>.ui-setup__row>.ui-setup__label,.ui-setup__optional .ui-setup__label{font-weight:450;color:var(--ui-text-muted)}.ui-setup__action{flex:none;display:inline-flex;gap:8px;margin:-4px 0 -4px auto}.ui-setup__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.ui-setup__body{padding:4px 0 12px 30px;font-size:var(--ui-fs-body, 13px);line-height:20px;color:var(--ui-text)}.ui-setup__body>:first-child{margin-top:0}.ui-setup__body>:last-child{margin-bottom:0}.ui-setup__optional{display:grid;gap:8px;padding-top:12px;border-top:1px solid var(--ui-border, #e5e7ea)}.ui-setup__subtitle{margin:0;font-size:13px;line-height:20px;font-weight:650;color:var(--ui-text-muted)}.ui-setup__x{flex:none;display:inline-grid;place-items:center;width:28px;height:28px;margin:-4px -4px -4px auto;padding:0;border:0;border-radius:8px;background:none;color:var(--ui-text-muted);cursor:pointer}.ui-setup__x svg{width:16px;height:16px}@media (pointer: coarse),(max-width: 47.9975em){summary.ui-setup__head,.ui-setup__row{min-height:44px;box-sizing:border-box}.ui-setup__x{width:44px;height:44px;margin:-12px -12px -12px auto}:is(.ui-setup__body,.ui-setup__action) :is(button,a.ui-btn){min-height:44px}}@media (prefers-reduced-motion: reduce){.ui-setup__head,.ui-setup__row,summary.ui-setup__head:after,.ui-setup__row:after,.ui-setup__bar::-webkit-progress-value{transition:none}}
|
package/src/setup-guide.tsx
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* Why it moved here (08/10/2026): gemform's Telegram alerts page drew its own numbered steps (1 The bot, 2 Where alerts
|
|
10
10
|
* go), the admin has this one in Polaris, and a third surface with a "connect this, then that" page would write a third.
|
|
11
11
|
*
|
|
12
|
+
* ONE checklist for the fleet (21/10/2026, Chris: "cái onboarding checklist của gemcommunity với của gemboss nút tick
|
|
13
|
+
* màu xanh progress bar nên đồng bộ nhé để dùng cùng một gemboss ui kit"). GemBoss' "Set up your store" and
|
|
14
|
+
* GemCommunity's "Start here" were two drawings of one thing: grey ✓ glyphs and a black bar on one, filled green ticks,
|
|
15
|
+
* an iris bar and a "✓ 2 completed" fold on the other. Both now render this, so the guide took what each did better:
|
|
16
|
+
* the filled green tick (todo is an empty ring), a bar in the same green (it counts the ticks), the steps already done
|
|
17
|
+
* folded behind one "✓ N completed" row, an "Optional" group under the counted steps (the admin's notes), a step's own
|
|
18
|
+
* control at the end of its row (`action`), and a × for a guide that is dismissed rather than folded (`onDismiss`).
|
|
19
|
+
*
|
|
12
20
|
* Native `<details>` all the way down, so a page that writes the markup by hand gets the same behaviour with no script:
|
|
13
21
|
* the guide is a `<details>` whose `<summary>` folds it away, each step is a `<details name>` of one group, which the
|
|
14
22
|
* browser keeps to one open at a time. The ROW reveals; what changes something is a button the surface puts in the
|
|
@@ -16,8 +24,10 @@
|
|
|
16
24
|
* nothing.
|
|
17
25
|
*
|
|
18
26
|
* The step open at first is the first one not done, decided ONCE, like the admin: a step's state changing while the
|
|
19
|
-
* person reads it must not slam it shut (finishing step 1 leaves its confirmation on screen). The
|
|
20
|
-
*
|
|
27
|
+
* person reads it must not slam it shut (finishing step 1 leaves its confirmation on screen). The fold is decided once
|
|
28
|
+
* for the same reason: a step finished while the guide is on screen stays where it is and turns green, it does not
|
|
29
|
+
* vanish into the fold under the person's eyes; the next visit folds it. The bar is a <progress>, not a width in a
|
|
30
|
+
* style attribute, so it fills under a CSP that forbids inline style.
|
|
21
31
|
*/
|
|
22
32
|
import "./setup-guide.css"; // its own entry (0.30.0)
|
|
23
33
|
import React, { useEffect, useId, useRef, useState } from "react";
|
|
@@ -33,32 +43,57 @@ export interface SetupStep {
|
|
|
33
43
|
state?: SetupState;
|
|
34
44
|
/** What the open step shows: a sentence and the control that does it. Without one the row is not a toggle. */
|
|
35
45
|
body?: React.ReactNode;
|
|
46
|
+
/** The control that does the step (21/10/2026): at the end of its row when the step has no body, under the body
|
|
47
|
+
* when it has one (a control inside the row would be a control inside a toggle). */
|
|
48
|
+
action?: React.ReactNode;
|
|
49
|
+
/** A class on the step, for the surface's own rule (GemCommunity hides its Telegram step where the rail asks). */
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SetupGuideLabels {
|
|
54
|
+
progress?: string;
|
|
55
|
+
notMeasured?: (n: number) => string;
|
|
56
|
+
/** The fold of the steps already done: "2 completed". */
|
|
57
|
+
completed?: (n: number) => string;
|
|
58
|
+
/** The heading of the `optional` group. */
|
|
59
|
+
optional?: string;
|
|
60
|
+
/** The × of a guide with `onDismiss`. */
|
|
61
|
+
dismiss?: string;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
const MARK: Record<SetupState, string> = { done: "✓", todo: "○", unknown: "?" };
|
|
39
65
|
|
|
40
|
-
export function SetupGuide({ title, description, steps, open, collapsible = true, rememberKey, group, labels, className }: {
|
|
66
|
+
export function SetupGuide({ title, description, steps, optional, open, collapsible = true, foldDone = true, onDismiss, rememberKey, group, labels, className }: {
|
|
41
67
|
title: React.ReactNode;
|
|
42
68
|
description?: React.ReactNode;
|
|
43
69
|
steps: readonly SetupStep[];
|
|
70
|
+
/** Steps worth doing that the count leaves out (the admin's "Optional" notes): under a divider, never progress. */
|
|
71
|
+
optional?: readonly SetupStep[];
|
|
44
72
|
/** The step to show open at first; defaults to the first one not done. */
|
|
45
73
|
open?: string;
|
|
46
74
|
/** false: the head is a plain heading, the guide cannot be folded away. */
|
|
47
75
|
collapsible?: boolean;
|
|
76
|
+
/** Fold the steps done when the guide first renders behind one "✓ N completed" row (default). false lists them. */
|
|
77
|
+
foldDone?: boolean;
|
|
78
|
+
/** Put the guide away with a × instead of folding it (the surface remembers and stops rendering it). A guide that
|
|
79
|
+
* can be dismissed does not also fold: two ways to put one card away is one too many. */
|
|
80
|
+
onDismiss?: () => void;
|
|
48
81
|
/** Remember a folded guide in localStorage under this key, so someone not setting up today is not asked again on every
|
|
49
82
|
* visit. Give each place its own key (folding it on one page is not folding it everywhere). */
|
|
50
83
|
rememberKey?: string;
|
|
51
84
|
/** The steps' shared `name`. Defaults to a useId(), unique within one React root; pass one when two guides render in
|
|
52
85
|
* separate roots or separate static renders. */
|
|
53
86
|
group?: string;
|
|
54
|
-
labels?:
|
|
87
|
+
labels?: SetupGuideLabels;
|
|
55
88
|
className?: string;
|
|
56
89
|
}) {
|
|
57
90
|
const auto = useId();
|
|
58
91
|
const name = group ?? `gb-setup${auto}`;
|
|
59
|
-
// Decided once (see the header): later renders keep whatever the person opened.
|
|
92
|
+
// Decided once (see the header): later renders keep whatever the person opened, and what was folded.
|
|
60
93
|
const [first] = useState(() => open ?? steps.find((s) => s.state !== "done" && s.body)?.id);
|
|
94
|
+
const [folded] = useState(() => new Set(foldDone ? steps.filter((s) => s.state === "done").map((s) => s.id) : []));
|
|
61
95
|
const guide = useRef<HTMLDetailsElement>(null);
|
|
96
|
+
const folds = collapsible && !onDismiss;
|
|
62
97
|
useEffect(() => {
|
|
63
98
|
const el = guide.current;
|
|
64
99
|
if (!el || !rememberKey) return undefined;
|
|
@@ -66,13 +101,32 @@ export function SetupGuide({ title, description, steps, open, collapsible = true
|
|
|
66
101
|
const onToggle = () => { try { localStorage.setItem(rememberKey, el.open ? "open" : "folded"); } catch { /* ignore */ } };
|
|
67
102
|
el.addEventListener("toggle", onToggle);
|
|
68
103
|
return () => el.removeEventListener("toggle", onToggle);
|
|
69
|
-
}, [rememberKey]);
|
|
104
|
+
}, [rememberKey, folds]);
|
|
70
105
|
|
|
71
106
|
if (!steps.length) return null; // nothing to set up is no guide, not a "0/0" card
|
|
72
107
|
const total = steps.length;
|
|
73
108
|
const done = steps.filter((s) => s.state === "done").length;
|
|
74
109
|
const unknown = steps.filter((s) => s.state === "unknown").length;
|
|
75
110
|
const all = done === total;
|
|
111
|
+
// Folded = done when the guide first rendered AND still done (a step that came undone is back in the list).
|
|
112
|
+
const inFold = steps.filter((s) => folded.has(s.id) && s.state === "done");
|
|
113
|
+
const listed = steps.filter((s) => !inFold.includes(s));
|
|
114
|
+
|
|
115
|
+
const step = (s: SetupStep) => {
|
|
116
|
+
const state = s.state ?? "todo";
|
|
117
|
+
const row = (as: "summary" | "div") => React.createElement(as, { className: cx("ui-setup__row", as === "div" && "ui-setup__row--static") },
|
|
118
|
+
<span className="ui-setup__mark" aria-hidden="true">{MARK[state]}</span>,
|
|
119
|
+
<span className="ui-setup__label">{s.title}</span>,
|
|
120
|
+
as === "div" && s.action ? <span className="ui-setup__action">{s.action}</span> : null);
|
|
121
|
+
if (!s.body) return <div key={s.id} className={cx("ui-setup__step", s.className)} data-state={state}>{row("div")}</div>;
|
|
122
|
+
return (
|
|
123
|
+
<details key={s.id} className={cx("ui-setup__step", s.className)} name={name} open={s.id === first} data-state={state}>
|
|
124
|
+
{row("summary")}
|
|
125
|
+
<div className="ui-setup__body">{s.body}{s.action ? <div className="ui-setup__actions">{s.action}</div> : null}</div>
|
|
126
|
+
</details>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
76
130
|
const head = (
|
|
77
131
|
<>
|
|
78
132
|
<h2 className="ui-setup__title">{title}</h2>
|
|
@@ -85,27 +139,43 @@ export function SetupGuide({ title, description, steps, open, collapsible = true
|
|
|
85
139
|
const inner = (
|
|
86
140
|
<div className="ui-setup__inner">
|
|
87
141
|
{description ? <p className="ui-setup__desc">{description}</p> : null}
|
|
88
|
-
<progress className=
|
|
142
|
+
<progress className="ui-setup__bar" value={done} max={total}
|
|
89
143
|
aria-label={labels?.progress ?? "Setup progress"}>{`${done}/${total}`}</progress>
|
|
90
144
|
<div className="ui-setup__steps">
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
103
|
-
})}
|
|
145
|
+
{inFold.length ? (
|
|
146
|
+
// Not in the steps' `name` group: opening the fold must not shut the step that is open below it.
|
|
147
|
+
<details className="ui-setup__done">
|
|
148
|
+
<summary className="ui-setup__row">
|
|
149
|
+
<span className="ui-setup__mark" aria-hidden="true">{MARK.done}</span>
|
|
150
|
+
<span className="ui-setup__label">{labels?.completed?.(inFold.length) ?? `${inFold.length} completed`}</span>
|
|
151
|
+
</summary>
|
|
152
|
+
<div className="ui-setup__steps">{inFold.map(step)}</div>
|
|
153
|
+
</details>
|
|
154
|
+
) : null}
|
|
155
|
+
{listed.map(step)}
|
|
104
156
|
</div>
|
|
157
|
+
{optional?.length ? (
|
|
158
|
+
<div className="ui-setup__optional">
|
|
159
|
+
<h3 className="ui-setup__subtitle">{labels?.optional ?? "Optional"}</h3>
|
|
160
|
+
<div className="ui-setup__steps">{optional.map(step)}</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : null}
|
|
105
163
|
</div>
|
|
106
164
|
);
|
|
107
|
-
if (!
|
|
108
|
-
return
|
|
165
|
+
if (!folds) {
|
|
166
|
+
return (
|
|
167
|
+
<section className={cx("ui-card ui-setup", className)}>
|
|
168
|
+
<div className="ui-setup__head">
|
|
169
|
+
{head}
|
|
170
|
+
{onDismiss ? (
|
|
171
|
+
<button type="button" className="ui-setup__x" aria-label={labels?.dismiss ?? "Dismiss"} onClick={onDismiss}>
|
|
172
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" /></svg>
|
|
173
|
+
</button>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
{inner}
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
109
179
|
}
|
|
110
180
|
return (
|
|
111
181
|
<details className={cx("ui-card ui-setup", className)} open ref={guide}>
|