@womp/kookie-ui 0.11.0 → 0.13.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/components.css +182 -192
- package/dist/cjs/components/chatbar.d.ts +26 -2
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js +2 -2
- package/dist/cjs/components/chatbar.js.map +3 -3
- package/dist/cjs/components/index.js +5 -5
- package/dist/cjs/components/index.js.map +3 -3
- package/dist/cjs/hooks/appearance-script.d.ts +16 -0
- package/dist/cjs/hooks/appearance-script.d.ts.map +1 -0
- package/dist/cjs/hooks/appearance-script.js +12 -0
- package/dist/cjs/hooks/appearance-script.js.map +7 -0
- package/dist/cjs/hooks/index.d.ts +3 -0
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.js +12 -2
- package/dist/cjs/hooks/index.js.map +4 -4
- package/dist/cjs/hooks/use-appearance.d.ts +30 -0
- package/dist/cjs/hooks/use-appearance.d.ts.map +1 -0
- package/dist/cjs/hooks/use-appearance.js +12 -0
- package/dist/cjs/hooks/use-appearance.js.map +7 -0
- package/dist/cjs/index.js +15 -5
- package/dist/cjs/index.js.map +4 -4
- package/dist/esm/chunk-2KZ4NI3X.js +1 -0
- package/dist/esm/chunk-EGMUQQRW.js +2 -0
- package/dist/esm/chunk-EGMUQQRW.js.map +7 -0
- package/dist/esm/chunk-HT7RFHLW.js +2 -0
- package/dist/esm/chunk-HT7RFHLW.js.map +7 -0
- package/dist/esm/chunk-IUER6BNH.js +12 -0
- package/dist/esm/chunk-IUER6BNH.js.map +7 -0
- package/dist/esm/components/chatbar.d.ts +26 -2
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js +1 -1
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/hooks/appearance-script.d.ts +16 -0
- package/dist/esm/hooks/appearance-script.d.ts.map +1 -0
- package/dist/esm/hooks/appearance-script.js +2 -0
- package/dist/esm/hooks/appearance-script.js.map +7 -0
- package/dist/esm/hooks/index.d.ts +3 -0
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.js +1 -1
- package/dist/esm/hooks/use-appearance.d.ts +30 -0
- package/dist/esm/hooks/use-appearance.d.ts.map +1 -0
- package/dist/esm/hooks/use-appearance.js +2 -0
- package/dist/esm/hooks/use-appearance.js.map +7 -0
- package/dist/esm/index.js +1 -1
- package/package.json +11 -1
- package/src/components/chatbar.css +75 -40
- package/src/components/chatbar.tsx +126 -49
- package/src/hooks/appearance-script.ts +28 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-appearance.ts +119 -0
- package/styles.css +182 -192
- package/dist/esm/chunk-EL7YGOTY.js +0 -1
- package/dist/esm/chunk-ENLFKWYV.js +0 -2
- package/dist/esm/chunk-ENLFKWYV.js.map +0 -7
- /package/dist/esm/{chunk-EL7YGOTY.js.map → chunk-2KZ4NI3X.js.map} +0 -0
package/dist/esm/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import"./chunk-
|
|
1
|
+
import"./chunk-2KZ4NI3X.js";import{a as No,b as Oo}from"./chunk-HT7RFHLW.js";import{a as Lo,b as Mo}from"./chunk-IUER6BNH.js";import"./chunk-C7EVYSWK.js";import{a as zo}from"./chunk-UYCPDGWA.js";import{b as Ao,c as Bo,d as Co,e as Do,f as Eo}from"./chunk-XLMTTSRO.js";import"./chunk-ZUOK3KFY.js";import{a as Fo}from"./chunk-CYU6BLZ5.js";import"./chunk-4WVNMGI5.js";import{a as Go}from"./chunk-YWENWZOU.js";import{c as qo}from"./chunk-ALWNS44Q.js";import"./chunk-MN3XALXD.js";import{a as so,b as uo}from"./chunk-D6PHTHF2.js";import{a as yo}from"./chunk-6G3MPL4A.js";import{a as wo}from"./chunk-VK7RNXUO.js";import{a as vo}from"./chunk-EE54S7YV.js";import{c as jo}from"./chunk-K3UAS7RW.js";import"./chunk-AUV6B4AH.js";import{h as ko}from"./chunk-36A5BYCN.js";import"./chunk-BALS3AVM.js";import{e as lo}from"./chunk-VNH7BJOA.js";import"./chunk-TZTUPQYK.js";import{a as no}from"./chunk-EPTYNVCU.js";import"./chunk-CLMOWP5T.js";import{a as go}from"./chunk-VZSQATP6.js";import{a as ho}from"./chunk-43A5GUSI.js";import"./chunk-XI5YXUCK.js";import{a as io}from"./chunk-KJUDZCYK.js";import"./chunk-AF6XKU3W.js";import{h as Io}from"./chunk-MJOXEUBT.js";import"./chunk-2EQWBT2S.js";import{p as Ho}from"./chunk-53SZ4Y23.js";import"./chunk-2CMM7MEL.js";import"./chunk-OJU5GCDZ.js";import{c as xo}from"./chunk-FG7XM7KJ.js";import"./chunk-FC2DBBJ3.js";import{h as bo}from"./chunk-VHECAEUP.js";import"./chunk-EBQZ3Z22.js";import{a as co}from"./chunk-VR7ZAFYN.js";import"./chunk-CRSHBENJ.js";import{g as Jo}from"./chunk-ZK7A2MZZ.js";import"./chunk-ADXZNG4F.js";import{a as ao}from"./chunk-OGXZHAQL.js";import"./chunk-NIX6Z3JG.js";import{c as mo}from"./chunk-2I7QRVJ5.js";import"./chunk-VLTXI4A7.js";import{a as po}from"./chunk-CIYF56YS.js";import"./chunk-STPMPY3X.js";import{a as to}from"./chunk-FHEHX7CE.js";import{a as oo}from"./chunk-XEZXKPLE.js";import{a as ro}from"./chunk-CDG5WH4U.js";import"./chunk-5XV4QEP5.js";import{a as eo}from"./chunk-GLERJXVS.js";import"./chunk-MCQCG6AS.js";import{c as fo}from"./chunk-CRTH5LNU.js";import"./chunk-OWMA6GTG.js";import{a as _}from"./chunk-YBSRX63K.js";import"./chunk-AGSSWWFI.js";import{i as Ko}from"./chunk-OIUCV5HK.js";import{e as $}from"./chunk-7UHKEJ7Q.js";import"./chunk-Y3A6ZU24.js";import{a as Y}from"./chunk-IHK3H2OM.js";import{a as X}from"./chunk-S5C5YRBS.js";import"./chunk-IBF5U4QW.js";import"./chunk-L6UXAITB.js";import"./chunk-GFOSP3PZ.js";import{a as Z}from"./chunk-CSKZZOI2.js";import"./chunk-JAMLW534.js";import{d as W}from"./chunk-MEO7MJMQ.js";import"./chunk-VJSZG35J.js";import{e as R}from"./chunk-RNTCHVFI.js";import{g as T}from"./chunk-7JJT42J2.js";import{a as S}from"./chunk-MHANZINI.js";import{o as U}from"./chunk-MZXHKM4V.js";import"./chunk-WOR3JCLX.js";import{a as V}from"./chunk-7FC7HF76.js";import"./chunk-4JQZUQY7.js";import{a as M}from"./chunk-N4GTDLRD.js";import{l as O}from"./chunk-T4OR62DQ.js";import{g as N}from"./chunk-WHFJ4JUN.js";import"./chunk-72AHPGPN.js";import"./chunk-RDAMYQFF.js";import{a as P}from"./chunk-4ME6O27S.js";import"./chunk-PPNWGWN3.js";import{n as Q}from"./chunk-PQKJNRMR.js";import"./chunk-OA7C34MJ.js";import"./chunk-NIIEIZKQ.js";import{c as J}from"./chunk-METRW4HG.js";import{a as I}from"./chunk-VX6ARMWP.js";import"./chunk-667BKCUG.js";import"./chunk-HAGKI7NA.js";import{c as K}from"./chunk-OMGZG52I.js";import"./chunk-V32EP3S7.js";import"./chunk-5SNHUVDA.js";import{a as L}from"./chunk-KFR3CK24.js";import"./chunk-UICSVJ3B.js";import"./chunk-KZWNGWIM.js";import{a as u}from"./chunk-4YFJWXL2.js";import{d as y}from"./chunk-CKMMUHKE.js";import"./chunk-OGVKSYLL.js";import{m as H}from"./chunk-EGMUQQRW.js";import{b as F,c as G}from"./chunk-L5PID6BU.js";import{a as z}from"./chunk-ISOLUTZF.js";import{a as E}from"./chunk-COWZAUZR.js";import"./chunk-5GXFBLBJ.js";import"./chunk-UBOOUDS4.js";import"./chunk-XPRPYCII.js";import"./chunk-5BWLY27A.js";import{a as A,b as B,c as C,d as D}from"./chunk-IX5HIHVI.js";import{a as w}from"./chunk-3R4YLW4A.js";import{a as v}from"./chunk-2N2AQNJH.js";import{a as k}from"./chunk-Y7LT3F6R.js";import{a as l}from"./chunk-HJU4I34X.js";import"./chunk-PS6NEG5A.js";import"./chunk-QFPHEY77.js";import{a as q}from"./chunk-KMPE7TJ6.js";import{a as n}from"./chunk-OLAWVEAI.js";import"./chunk-BPVVFHYD.js";import"./chunk-QQ62LO3X.js";import{a as s}from"./chunk-MS5J4MF6.js";import"./chunk-FGORMEAO.js";import"./chunk-ROSWI2CL.js";import{a as b}from"./chunk-ERNM3KVF.js";import{f as c}from"./chunk-Y5RCH23Z.js";import"./chunk-EDKQURDU.js";import{h}from"./chunk-IYCPYN4Z.js";import"./chunk-MJJ7XDIP.js";import{a as g}from"./chunk-5WRCK4R7.js";import"./chunk-4XKK32M4.js";import"./chunk-XKBLNVOD.js";import"./chunk-GCUGJXI2.js";import{f as j}from"./chunk-54JFIP5S.js";import{a as i}from"./chunk-JDGJSNWT.js";import"./chunk-OO652UQW.js";import{a as d}from"./chunk-GO7OHBHW.js";import"./chunk-PZ7OYATT.js";import"./chunk-G2CKYO2I.js";import"./chunk-AXUDFYLY.js";import"./chunk-KNQBAOMV.js";import"./chunk-FD2TR343.js";import"./chunk-WYADYDPN.js";import"./chunk-ZE2KAVXW.js";import"./chunk-ZWZGUBL4.js";import"./chunk-7RQNB63F.js";import{a as t,b as x,c as a}from"./chunk-SEY4NA45.js";import"./chunk-WAURCNAS.js";import"./chunk-F22ZRNWL.js";import"./chunk-XYVLQDGR.js";import"./chunk-YAQBIO2B.js";import"./chunk-AGNIFHOE.js";import"./chunk-HCNO7PX6.js";import"./chunk-GMHA5CFH.js";import"./chunk-A2XVR7GL.js";import"./chunk-BZZXXZ3O.js";import"./chunk-2LYX5S7H.js";import"./chunk-HWQ3NTJP.js";import"./chunk-3J6AUDID.js";import"./chunk-4LDVSTXI.js";import{a as f}from"./chunk-HF5WY5YE.js";import"./chunk-3KB2XWBE.js";import{a as m}from"./chunk-AGAQC4HH.js";import"./chunk-5T6K7VPP.js";import{a as e}from"./chunk-FHOCUZDR.js";import"./chunk-SMGDH54W.js";import"./chunk-SNR5VFWG.js";import{b as o,c as r}from"./chunk-FWB47YCH.js";import"./chunk-K53W3ELO.js";import"./chunk-TTFUM47A.js";import"./chunk-VX44UTQ5.js";import"./chunk-ERGJN7FJ.js";import"./chunk-5TXQLK4G.js";import"./chunk-JFWMAYEH.js";import"./chunk-KRJTW535.js";import"./chunk-F6V7GBNQ.js";import"./chunk-3KSGRY6I.js";import"./chunk-ZOY43B55.js";import{a as p}from"./chunk-E67CIG6A.js";import"./chunk-ARIV4AT5.js";import"./chunk-3XSB7YIM.js";import"./chunk-YA5DF67M.js";import"./chunk-GSTOGDAJ.js";import"./chunk-6FLLFCBU.js";import"./chunk-AX5S56DS.js";import"./chunk-E23RKOQJ.js";import"./chunk-JUY7CBSL.js";import"./chunk-WRWNN2NW.js";import"./chunk-QM3RCK5P.js";import"./chunk-RZCX6IBE.js";import"./chunk-NZJGC6ZW.js";import"./chunk-WMGIFPI2.js";import"./chunk-CFNESRYI.js";import"./chunk-ZNK6AH2G.js";import"./chunk-JSW6J6RS.js";import"./chunk-OS3FCYZY.js";export{Lo as APPEARANCE_STORAGE_KEY,b as AccessibleIcon,c as Accordion,h as AlertDialog,j as AppBar,k as AspectRatio,l as Avatar,q as Badge,s as Blockquote,u as Box,w as Button,y as Callout,i as Card,H as Chatbar,L as Checkbox,J as CheckboxCards,K as CheckboxGroup,C as ChevronDownIcon,M as Code,O as Combobox,G as Confirmation,P as Container,Q as ContextMenu,R as DataList,T as Dialog,U as DropdownMenu,V as Em,e as Flex,I as Grid,d as Heading,W as HoverCard,z as IconButton,Y as Image,Z as Inset,p as Kbd,_ as Link,Ko as MessageScroller,$ as Navbar,F as Notice,N as Popover,oo as Portal,ro as Progress,eo as Quote,po as Radio,fo as RadioCards,mo as RadioGroup,to as Reset,E as ScrollArea,ao as Section,xo as SegmentedControl,bo as Select,co as Separator,Jo as Sheet,Io as Shell,Ho as Sidebar,X as Skeleton,go as Slider,o as Slot,r as Slottable,f as Spinner,ho as Strong,io as Switch,jo as TabNav,ko as Table,lo as Tabs,g as Text,no as TextArea,qo as TextField,a as Theme,t as ThemeContext,so as ThemePanel,uo as ThemePanelContent,B as ThickCheckIcon,D as ThickChevronRightIcon,A as ThickDividerHorizontalIcon,yo as ToggleButton,zo as ToggleIconButton,Eo as Toolbar,Bo as ToolbarButton,Ao as ToolbarGroup,Co as ToolbarSeparator,Do as ToolbarTitle,n as Tooltip,Fo as UserCard,Go as VirtualMenu,m as VisuallyHidden,Mo as appearanceScript,No as setAppearance,Oo as useAppearance,S as useBodyPointerEventsCleanup,vo as useLiveAnnouncer,x as useThemeContext,wo as useToggleState,v as useTooltipWrapper};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@womp/kookie-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Kookie UI v1: a React component library with design tokens, flexible theming, a full layout shell, and comprehensive documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -48,6 +48,16 @@
|
|
|
48
48
|
"default": "./dist/esm/components/*.js"
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
+
"./appearance-script": {
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/cjs/hooks/appearance-script.d.ts",
|
|
54
|
+
"default": "./dist/cjs/hooks/appearance-script.js"
|
|
55
|
+
},
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/esm/hooks/appearance-script.d.ts",
|
|
58
|
+
"default": "./dist/esm/hooks/appearance-script.js"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
51
61
|
"./helpers": {
|
|
52
62
|
"require": {
|
|
53
63
|
"types": "./dist/cjs/helpers/index.d.ts",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
/* Defaults */
|
|
10
10
|
.rt-ChatbarRoot {
|
|
11
|
-
--chatbar-
|
|
11
|
+
--chatbar-item-lead: var(--space-5);
|
|
12
|
+
--chatbar-item-lead-radius: var(--radius-2);
|
|
13
|
+
--chatbar-item-padding: var(--space-1);
|
|
12
14
|
/* A column: notices, then the box (which fills any height the consumer gives the root), then context. */
|
|
13
15
|
display: flex;
|
|
14
16
|
flex-direction: column;
|
|
@@ -116,58 +118,97 @@
|
|
|
116
118
|
--scrollarea-scrollbar-size: 3px;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/*
|
|
121
|
+
/*
|
|
122
|
+
* Attachments and items are one pill: a lead tile (thumbnail or tinted icon),
|
|
123
|
+
* a label and a remove button. A custom `renderAttachment` keeps only the
|
|
124
|
+
* plain .rt-ChatbarAttachment hook.
|
|
125
|
+
*/
|
|
126
|
+
.rt-ChatbarAttachment {
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.rt-ChatbarItem {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: var(--space-2);
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
box-sizing: border-box;
|
|
136
|
+
max-width: 240px;
|
|
137
|
+
padding: var(--chatbar-item-padding);
|
|
138
|
+
padding-right: calc(var(--chatbar-item-padding) + var(--space-1));
|
|
139
|
+
border: 1px solid var(--gray-a5);
|
|
140
|
+
border-radius: calc(var(--chatbar-item-lead-radius) + var(--chatbar-item-padding));
|
|
141
|
+
background-color: var(--color-panel-solid);
|
|
142
|
+
box-shadow: 0 1px 2px var(--gray-a3);
|
|
143
|
+
color: var(--gray-12);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.rt-ChatbarItem:where([data-status='error']) {
|
|
147
|
+
border-color: var(--red-a7);
|
|
148
|
+
}
|
|
120
149
|
|
|
121
|
-
.rt-
|
|
122
|
-
height: var(--chatbar-
|
|
123
|
-
width: var(--chatbar-
|
|
124
|
-
min-width: var(--chatbar-attachment-thumb);
|
|
150
|
+
.rt-ChatbarItemLead {
|
|
151
|
+
height: var(--chatbar-item-lead);
|
|
152
|
+
width: var(--chatbar-item-lead);
|
|
125
153
|
flex-shrink: 0;
|
|
126
|
-
border-radius: var(--radius
|
|
154
|
+
border-radius: var(--chatbar-item-lead-radius);
|
|
127
155
|
overflow: hidden;
|
|
128
156
|
display: inline-flex;
|
|
129
157
|
align-items: center;
|
|
130
158
|
justify-content: center;
|
|
159
|
+
background: var(--accent-a3);
|
|
160
|
+
color: var(--accent-11);
|
|
161
|
+
|
|
162
|
+
/* stylelint-disable-next-line selector-max-type */
|
|
163
|
+
& :where(svg) {
|
|
164
|
+
width: calc(var(--chatbar-item-lead) * 0.6);
|
|
165
|
+
height: calc(var(--chatbar-item-lead) * 0.6);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
&:where([data-kind='image']) {
|
|
169
|
+
background: var(--gray-a3);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* An attachment's file icon and spinner stay neutral */
|
|
174
|
+
.rt-ChatbarAttachment .rt-ChatbarItemLead:where([data-kind='icon']) {
|
|
131
175
|
background: var(--gray-a3);
|
|
176
|
+
color: var(--gray-11);
|
|
132
177
|
}
|
|
133
178
|
|
|
134
179
|
.rt-ChatbarAttachmentImage {
|
|
180
|
+
width: 100%;
|
|
135
181
|
height: 100%;
|
|
136
|
-
|
|
137
|
-
object-fit: contain;
|
|
182
|
+
object-fit: cover;
|
|
138
183
|
max-width: none;
|
|
139
184
|
}
|
|
140
185
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.rt-ChatbarAttachment {
|
|
144
|
-
position: relative;
|
|
145
|
-
background-color: var(--gray-1);
|
|
146
|
-
border-radius: var(--radius-1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/* Maximum width for non-image attachments */
|
|
150
|
-
.rt-ChatbarAttachment:where([data-kind='file']) {
|
|
151
|
-
max-width: 200px;
|
|
186
|
+
.rt-ChatbarItemLabel {
|
|
187
|
+
min-width: 0;
|
|
152
188
|
}
|
|
153
189
|
|
|
154
|
-
.rt-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
right: var(--chatbar-remove-offset);
|
|
190
|
+
.rt-ChatbarItemRemove {
|
|
191
|
+
flex-shrink: 0;
|
|
192
|
+
margin-left: calc(var(--space-1) * -1);
|
|
158
193
|
}
|
|
159
194
|
|
|
160
|
-
/* Size-dependent radius for attachment container and preview */
|
|
161
195
|
@breakpoints {
|
|
162
|
-
.rt-ChatbarRoot
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
196
|
+
.rt-ChatbarRoot {
|
|
197
|
+
&:where(.rt-r-size-1) {
|
|
198
|
+
--chatbar-item-lead: var(--space-4);
|
|
199
|
+
--chatbar-item-lead-radius: var(--radius-1);
|
|
200
|
+
--chatbar-item-padding: var(--space-1);
|
|
201
|
+
}
|
|
202
|
+
&:where(.rt-r-size-2) {
|
|
203
|
+
--chatbar-item-lead: var(--space-5);
|
|
204
|
+
--chatbar-item-lead-radius: var(--radius-2);
|
|
205
|
+
--chatbar-item-padding: var(--space-1);
|
|
206
|
+
}
|
|
207
|
+
&:where(.rt-r-size-3) {
|
|
208
|
+
--chatbar-item-lead: calc(var(--space-5) + var(--space-1));
|
|
209
|
+
--chatbar-item-lead-radius: var(--radius-2);
|
|
210
|
+
--chatbar-item-padding: calc(var(--space-1) * 1.5);
|
|
211
|
+
}
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
|
|
@@ -575,24 +616,18 @@
|
|
|
575
616
|
&:where(.rt-r-size-1) {
|
|
576
617
|
font-size: var(--font-size-1);
|
|
577
618
|
line-height: var(--line-height-1);
|
|
578
|
-
--chatbar-attachment-thumb: var(--space-8);
|
|
579
|
-
--chatbar-remove-offset: calc(var(--space-1) * 0.5);
|
|
580
619
|
/* --chatbar-attachments-radius: min(var(--radius-5), calc(var(--radius-1) + var(--space-1))); */
|
|
581
620
|
--chatbar-attachments-radius: var(--radius-1);
|
|
582
621
|
}
|
|
583
622
|
&:where(.rt-r-size-2) {
|
|
584
623
|
font-size: var(--font-size-2);
|
|
585
624
|
line-height: var(--line-height-2);
|
|
586
|
-
--chatbar-attachment-thumb: var(--space-9);
|
|
587
|
-
--chatbar-remove-offset: var(--space-1);
|
|
588
625
|
/* --chatbar-attachments-radius: min(var(--radius-6), calc(var(--radius-2) + var(--space-2))); */
|
|
589
626
|
--chatbar-attachments-radius: var(--radius-2);
|
|
590
627
|
}
|
|
591
628
|
&:where(.rt-r-size-3) {
|
|
592
629
|
font-size: var(--font-size-3);
|
|
593
630
|
line-height: var(--line-height-3);
|
|
594
|
-
--chatbar-attachment-thumb: var(--space-9);
|
|
595
|
-
--chatbar-remove-offset: var(--space-2);
|
|
596
631
|
/* --chatbar-attachments-radius: min(var(--radius-7), calc(var(--radius-3) + var(--space-3))); */
|
|
597
632
|
--chatbar-attachments-radius: var(--radius-3);
|
|
598
633
|
}
|
|
@@ -1030,18 +1030,22 @@ InlineEnd.displayName = 'Chatbar.InlineEnd';
|
|
|
1030
1030
|
/**
|
|
1031
1031
|
* Renders a horizontally scrollable list of attachments above the inline row.
|
|
1032
1032
|
* Hidden when empty unless `forceMount`. Override per-item with `renderAttachment`.
|
|
1033
|
+
* `Chatbar.Item`s passed as children follow the attachments in the same row.
|
|
1033
1034
|
*/
|
|
1034
1035
|
interface AttachmentsRowProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
1035
1036
|
asChild?: boolean;
|
|
1036
1037
|
forceMount?: boolean;
|
|
1037
1038
|
/** If provided, custom-render a tile; otherwise default tile is used */
|
|
1038
1039
|
renderAttachment?: (attachment: ChatbarAttachment) => React.ReactNode;
|
|
1040
|
+
/** `Chatbar.Item`s: things the message refers to that are not files. */
|
|
1041
|
+
children?: React.ReactNode;
|
|
1039
1042
|
}
|
|
1040
1043
|
|
|
1041
1044
|
const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((props, forwardedRef) => {
|
|
1042
|
-
const { asChild, forceMount, renderAttachment, className, ...divProps } = props;
|
|
1045
|
+
const { asChild, forceMount, renderAttachment, className, children, ...divProps } = props;
|
|
1043
1046
|
const ctx = useChatbarContext();
|
|
1044
|
-
|
|
1047
|
+
const hasItems = React.Children.toArray(children).length > 0;
|
|
1048
|
+
if (ctx.attachments.length === 0 && !hasItems && !forceMount) return null;
|
|
1045
1049
|
const Comp = asChild ? Slot : 'div';
|
|
1046
1050
|
return (
|
|
1047
1051
|
<m.div layout="position" transition={LAYOUT_TRANSITION}>
|
|
@@ -1053,6 +1057,7 @@ const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((pr
|
|
|
1053
1057
|
{renderAttachment?.(att)}
|
|
1054
1058
|
</Attachment>
|
|
1055
1059
|
))}
|
|
1060
|
+
{children}
|
|
1056
1061
|
</Flex>
|
|
1057
1062
|
</ScrollArea>
|
|
1058
1063
|
</Comp>
|
|
@@ -1061,6 +1066,50 @@ const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((pr
|
|
|
1061
1066
|
});
|
|
1062
1067
|
AttachmentsRow.displayName = 'Chatbar.AttachmentsRow';
|
|
1063
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Moves focus off a pill that is about to unmount, to the next pill's remove
|
|
1071
|
+
* button, the previous one's, or the text, so keyboard users are not dropped
|
|
1072
|
+
* to <body>.
|
|
1073
|
+
*/
|
|
1074
|
+
function moveFocusOffPill(button: HTMLButtonElement, textareaRef: React.RefObject<HTMLTextAreaElement | null>) {
|
|
1075
|
+
const pill = button.closest('[role="listitem"]');
|
|
1076
|
+
const pills = pill?.parentElement ? Array.from(pill.parentElement.children) : [];
|
|
1077
|
+
const index = pill ? pills.indexOf(pill) : -1;
|
|
1078
|
+
const neighbour = pills[index + 1] ?? pills[index - 1];
|
|
1079
|
+
const nextButton = neighbour?.querySelector<HTMLButtonElement>('.rt-ChatbarItemRemove');
|
|
1080
|
+
if (nextButton) nextButton.focus();
|
|
1081
|
+
else textareaRef.current?.focus({ preventScroll: true });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** The pill an attachment and an item share: a lead tile, a label and ×. */
|
|
1085
|
+
const Pill: React.FC<{
|
|
1086
|
+
lead: React.ReactNode;
|
|
1087
|
+
leadKind: 'image' | 'icon';
|
|
1088
|
+
label: React.ReactNode;
|
|
1089
|
+
/** Read after the label by screen readers only, e.g. a file's size. */
|
|
1090
|
+
detail?: string;
|
|
1091
|
+
removeLabel: string;
|
|
1092
|
+
onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
1093
|
+
}> = ({ lead, leadKind, label, detail, removeLabel, onRemove }) => {
|
|
1094
|
+
const ctx = useChatbarContext();
|
|
1095
|
+
return (
|
|
1096
|
+
<>
|
|
1097
|
+
<span className="rt-ChatbarItemLead" data-kind={leadKind} aria-hidden>
|
|
1098
|
+
{lead}
|
|
1099
|
+
</span>
|
|
1100
|
+
<Text className="rt-ChatbarItemLabel" size={ctx.size} truncate>
|
|
1101
|
+
{label}
|
|
1102
|
+
{detail ? <VisuallyHidden>, {detail}</VisuallyHidden> : null}
|
|
1103
|
+
</Text>
|
|
1104
|
+
{onRemove && (
|
|
1105
|
+
<IconButton className="rt-ChatbarItemRemove" aria-label={removeLabel} size="1" variant="ghost" color="gray" onClick={onRemove}>
|
|
1106
|
+
<CloseIcon />
|
|
1107
|
+
</IconButton>
|
|
1108
|
+
)}
|
|
1109
|
+
</>
|
|
1110
|
+
);
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1064
1113
|
/** Default tile renderer for a single attachment. */
|
|
1065
1114
|
interface AttachmentProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
1066
1115
|
attachment: ChatbarAttachment;
|
|
@@ -1079,71 +1128,98 @@ const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>((props, for
|
|
|
1079
1128
|
const locked = ctx.disabled || ctx.readOnly;
|
|
1080
1129
|
|
|
1081
1130
|
const handleRemove = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1082
|
-
|
|
1083
|
-
const tile = event.currentTarget.closest('[role="listitem"]');
|
|
1084
|
-
const tiles = tile?.parentElement ? Array.from(tile.parentElement.children) : [];
|
|
1085
|
-
const index = tile ? tiles.indexOf(tile) : -1;
|
|
1086
|
-
const neighbour = tiles[index + 1] ?? tiles[index - 1];
|
|
1087
|
-
const nextButton = neighbour?.querySelector<HTMLButtonElement>('.rt-ChatbarAttachmentRemove');
|
|
1088
|
-
if (nextButton) nextButton.focus();
|
|
1089
|
-
else ctx.textareaRef.current?.focus({ preventScroll: true });
|
|
1131
|
+
moveFocusOffPill(event.currentTarget, ctx.textareaRef);
|
|
1090
1132
|
ctx.removeAttachment(attachment.id);
|
|
1091
1133
|
};
|
|
1092
1134
|
|
|
1135
|
+
const lead =
|
|
1136
|
+
attachment.status === 'uploading' ? (
|
|
1137
|
+
<Spinner size="1" />
|
|
1138
|
+
) : isImage ? (
|
|
1139
|
+
<img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" />
|
|
1140
|
+
) : (
|
|
1141
|
+
<FileTextIcon />
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1093
1144
|
return (
|
|
1094
1145
|
<Comp
|
|
1095
1146
|
{...divProps}
|
|
1096
1147
|
ref={forwardedRef}
|
|
1097
|
-
className={classNames('rt-ChatbarAttachment', className)}
|
|
1148
|
+
className={classNames(children ? 'rt-ChatbarAttachment' : 'rt-ChatbarAttachment rt-ChatbarItem', className)}
|
|
1098
1149
|
role="listitem"
|
|
1099
1150
|
data-kind={isImage ? 'image' : 'file'}
|
|
1100
1151
|
data-status={attachment.status}
|
|
1101
1152
|
title={attachment.name}
|
|
1102
1153
|
>
|
|
1103
1154
|
{children ?? (
|
|
1104
|
-
<
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
{
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
</VisuallyHidden>
|
|
1113
|
-
) : (
|
|
1114
|
-
<Flex direction="column" gap="0" minWidth="0">
|
|
1115
|
-
<Text size={ctx.size} weight="medium" truncate>
|
|
1116
|
-
{attachment.name}
|
|
1117
|
-
</Text>
|
|
1118
|
-
<Text
|
|
1119
|
-
size="1"
|
|
1120
|
-
color={attachment.status === 'error' ? 'red' : undefined}
|
|
1121
|
-
emphasis={attachment.status === 'error' ? undefined : 'medium'}
|
|
1122
|
-
>
|
|
1123
|
-
{sizeLabel}
|
|
1124
|
-
{statusLabel ? ` · ${statusLabel}` : ''}
|
|
1125
|
-
</Text>
|
|
1126
|
-
</Flex>
|
|
1127
|
-
)}
|
|
1128
|
-
{!locked && (
|
|
1129
|
-
<IconButton
|
|
1130
|
-
className="rt-ChatbarAttachmentRemove"
|
|
1131
|
-
aria-label={`Remove ${attachment.name}`}
|
|
1132
|
-
size="1"
|
|
1133
|
-
variant="classic"
|
|
1134
|
-
color="gray"
|
|
1135
|
-
onClick={handleRemove}
|
|
1136
|
-
>
|
|
1137
|
-
<CloseIcon />
|
|
1138
|
-
</IconButton>
|
|
1139
|
-
)}
|
|
1140
|
-
</Flex>
|
|
1155
|
+
<Pill
|
|
1156
|
+
lead={lead}
|
|
1157
|
+
leadKind={isImage && attachment.status !== 'uploading' ? 'image' : 'icon'}
|
|
1158
|
+
label={attachment.name}
|
|
1159
|
+
detail={statusLabel ? `${sizeLabel}, ${statusLabel}` : sizeLabel}
|
|
1160
|
+
removeLabel={`Remove ${attachment.name}`}
|
|
1161
|
+
onRemove={locked ? undefined : handleRemove}
|
|
1162
|
+
/>
|
|
1141
1163
|
)}
|
|
1142
1164
|
</Comp>
|
|
1143
1165
|
);
|
|
1144
1166
|
});
|
|
1145
1167
|
Attachment.displayName = 'Chatbar.Attachment';
|
|
1146
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* Something the message refers to that is not a file: a scene object, a
|
|
1171
|
+
* picked mode, a selection. The same pill as an attachment, placed in
|
|
1172
|
+
* `Chatbar.AttachmentsRow` as a child. The app owns the list, so removing one
|
|
1173
|
+
* calls `onRemove` and nothing else; leave it out for a pill that cannot be
|
|
1174
|
+
* removed.
|
|
1175
|
+
*/
|
|
1176
|
+
interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'color'> {
|
|
1177
|
+
/** The label. Also names the remove button: "Remove {label}". */
|
|
1178
|
+
children: string;
|
|
1179
|
+
/** Drawn in the lead tile, tinted with `color`. */
|
|
1180
|
+
icon?: React.ReactNode;
|
|
1181
|
+
/** A thumbnail for the lead tile, in place of the icon. */
|
|
1182
|
+
image?: string;
|
|
1183
|
+
/** Tint of the icon tile. Defaults to gray. */
|
|
1184
|
+
color?: (typeof accentColors)[number];
|
|
1185
|
+
onRemove?: () => void;
|
|
1186
|
+
/** Overrides the remove button's label. */
|
|
1187
|
+
removeLabel?: string;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const Item = React.forwardRef<HTMLDivElement, ItemProps>((props, forwardedRef) => {
|
|
1191
|
+
const { children, icon, image, color, onRemove, removeLabel, className, ...divProps } = props;
|
|
1192
|
+
const ctx = useChatbarContext();
|
|
1193
|
+
const locked = ctx.disabled || ctx.readOnly;
|
|
1194
|
+
|
|
1195
|
+
const handleRemove = onRemove
|
|
1196
|
+
? (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1197
|
+
moveFocusOffPill(event.currentTarget, ctx.textareaRef);
|
|
1198
|
+
onRemove();
|
|
1199
|
+
}
|
|
1200
|
+
: undefined;
|
|
1201
|
+
|
|
1202
|
+
return (
|
|
1203
|
+
<div
|
|
1204
|
+
{...divProps}
|
|
1205
|
+
ref={forwardedRef}
|
|
1206
|
+
className={classNames('rt-ChatbarItem', className)}
|
|
1207
|
+
role="listitem"
|
|
1208
|
+
data-accent-color={image ? undefined : (color ?? 'gray')}
|
|
1209
|
+
title={children}
|
|
1210
|
+
>
|
|
1211
|
+
<Pill
|
|
1212
|
+
lead={image ? <img className="rt-ChatbarAttachmentImage" src={image} alt="" /> : icon}
|
|
1213
|
+
leadKind={image ? 'image' : 'icon'}
|
|
1214
|
+
label={children}
|
|
1215
|
+
removeLabel={removeLabel ?? `Remove ${children}`}
|
|
1216
|
+
onRemove={locked ? undefined : handleRemove}
|
|
1217
|
+
/>
|
|
1218
|
+
</div>
|
|
1219
|
+
);
|
|
1220
|
+
});
|
|
1221
|
+
Item.displayName = 'Chatbar.Item';
|
|
1222
|
+
|
|
1147
1223
|
interface AttachTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
1148
1224
|
asChild?: boolean;
|
|
1149
1225
|
/** Overrides Root `accept` for files chosen through this trigger */
|
|
@@ -1346,13 +1422,14 @@ const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef
|
|
|
1346
1422
|
});
|
|
1347
1423
|
Send.displayName = 'Chatbar.Send';
|
|
1348
1424
|
|
|
1349
|
-
export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, AttachTrigger, Row, RowStart, RowEnd, Send };
|
|
1425
|
+
export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, Item, AttachTrigger, Row, RowStart, RowEnd, Send };
|
|
1350
1426
|
export type {
|
|
1351
1427
|
RootProps as ChatbarRootProps,
|
|
1352
1428
|
TextareaProps as ChatbarTextareaProps,
|
|
1353
1429
|
InlineSlotProps as ChatbarInlineSlotProps,
|
|
1354
1430
|
AttachmentsRowProps as ChatbarAttachmentsRowProps,
|
|
1355
1431
|
AttachmentProps as ChatbarAttachmentProps,
|
|
1432
|
+
ItemProps as ChatbarItemProps,
|
|
1356
1433
|
AttachTriggerProps as ChatbarAttachTriggerProps,
|
|
1357
1434
|
RowProps as ChatbarRowProps,
|
|
1358
1435
|
RowSlotProps as ChatbarRowSlotProps,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-paint half of light and dark mode (see `use-appearance.ts`). Kept in its own module
|
|
3
|
+
* with no client directive, so a server component such as a Next.js root layout can import the
|
|
4
|
+
* string and inline it in `<head>`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const APPEARANCE_STORAGE_KEY = 'kookie-appearance';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inline this in `<head>`, before any stylesheet paints, e.g.
|
|
11
|
+
* `<script dangerouslySetInnerHTML={{ __html: appearanceScript }} />`.
|
|
12
|
+
*
|
|
13
|
+
* Storage is read inside its own try/catch: browsers that block site data throw on access,
|
|
14
|
+
* and that must not skip the stamp. `matchMedia` always answers.
|
|
15
|
+
*/
|
|
16
|
+
const appearanceScript = `(function () {
|
|
17
|
+
var e = document.documentElement;
|
|
18
|
+
var a = null;
|
|
19
|
+
try { a = localStorage.getItem(${JSON.stringify(APPEARANCE_STORAGE_KEY)}); } catch (err) {}
|
|
20
|
+
var dark = a === "light" ? false : a === "dark" ? true : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
21
|
+
var mode = dark ? "dark" : "light";
|
|
22
|
+
e.classList.remove("light", "dark");
|
|
23
|
+
e.classList.add(mode);
|
|
24
|
+
e.setAttribute("data-appearance", mode);
|
|
25
|
+
e.style.colorScheme = mode;
|
|
26
|
+
})();`;
|
|
27
|
+
|
|
28
|
+
export { appearanceScript, APPEARANCE_STORAGE_KEY };
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,3 +2,6 @@ export { useLiveAnnouncer } from './use-live-announcer.js';
|
|
|
2
2
|
export { useBodyPointerEventsCleanup } from './use-body-pointer-events-cleanup.js';
|
|
3
3
|
export { useTooltipWrapper } from './use-tooltip-wrapper.js';
|
|
4
4
|
export { useToggleState } from './use-toggle-state.js';
|
|
5
|
+
export { useAppearance, setAppearance } from './use-appearance.js';
|
|
6
|
+
export { appearanceScript, APPEARANCE_STORAGE_KEY } from './appearance-script.js';
|
|
7
|
+
export type { AppearanceChoice, ResolvedAppearance } from './use-appearance.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { APPEARANCE_STORAGE_KEY } from './appearance-script.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Light and dark mode, decided before the first paint and kept on `<html>`.
|
|
8
|
+
*
|
|
9
|
+
* The server cannot know the visitor's mode, so `appearanceScript` runs synchronously in
|
|
10
|
+
* `<head>`: the stored choice first, `prefers-color-scheme` otherwise. It writes the `light`
|
|
11
|
+
* or `dark` class (which every token and component stylesheet already keys off) and
|
|
12
|
+
* `data-appearance` on `<html>`. The root `<Theme>` keeps `appearance="inherit"`, so it
|
|
13
|
+
* writes no class of its own and `<html>` is the one source of truth. Nested themes and
|
|
14
|
+
* portals inherit it from there.
|
|
15
|
+
*
|
|
16
|
+
* `useAppearance` is the client half: an external store over `<html>` and the same storage
|
|
17
|
+
* key. It never mirrors the mode into React state that could disagree with the DOM.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type AppearanceChoice = 'system' | 'light' | 'dark';
|
|
21
|
+
type ResolvedAppearance = 'light' | 'dark';
|
|
22
|
+
|
|
23
|
+
const listeners = new Set<() => void>();
|
|
24
|
+
|
|
25
|
+
function emit() {
|
|
26
|
+
for (const listener of listeners) listener();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readChoice(): AppearanceChoice {
|
|
30
|
+
try {
|
|
31
|
+
const value = globalThis.localStorage?.getItem(APPEARANCE_STORAGE_KEY);
|
|
32
|
+
return value === 'light' || value === 'dark' ? value : 'system';
|
|
33
|
+
} catch {
|
|
34
|
+
return 'system';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function systemAppearance(): ResolvedAppearance {
|
|
39
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stamp(mode: ResolvedAppearance) {
|
|
43
|
+
const root = document.documentElement;
|
|
44
|
+
root.classList.remove('light', 'dark');
|
|
45
|
+
root.classList.add(mode);
|
|
46
|
+
root.setAttribute('data-appearance', mode);
|
|
47
|
+
root.style.colorScheme = mode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readResolved(): ResolvedAppearance {
|
|
51
|
+
const root = document.documentElement;
|
|
52
|
+
if (root.classList.contains('dark') || root.classList.contains('dark-theme')) return 'dark';
|
|
53
|
+
if (root.classList.contains('light') || root.classList.contains('light-theme')) return 'light';
|
|
54
|
+
return systemAppearance();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let snapshot: { choice: AppearanceChoice; resolved: ResolvedAppearance } | null = null;
|
|
58
|
+
|
|
59
|
+
function getSnapshot() {
|
|
60
|
+
const choice = readChoice();
|
|
61
|
+
const resolved = readResolved();
|
|
62
|
+
if (!snapshot || snapshot.choice !== choice || snapshot.resolved !== resolved) {
|
|
63
|
+
snapshot = { choice, resolved };
|
|
64
|
+
}
|
|
65
|
+
return snapshot;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const serverSnapshot = { choice: 'system' as AppearanceChoice, resolved: 'light' as ResolvedAppearance };
|
|
69
|
+
|
|
70
|
+
function subscribe(listener: () => void) {
|
|
71
|
+
listeners.add(listener);
|
|
72
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
73
|
+
// With no stored choice, follow the system as it changes.
|
|
74
|
+
const onSystemChange = () => {
|
|
75
|
+
if (readChoice() === 'system') {
|
|
76
|
+
stamp(systemAppearance());
|
|
77
|
+
emit();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
// Another tab changed the choice.
|
|
81
|
+
const onStorage = (event: StorageEvent) => {
|
|
82
|
+
if (event.key !== APPEARANCE_STORAGE_KEY) return;
|
|
83
|
+
const choice = readChoice();
|
|
84
|
+
stamp(choice === 'system' ? systemAppearance() : choice);
|
|
85
|
+
emit();
|
|
86
|
+
};
|
|
87
|
+
media.addEventListener('change', onSystemChange);
|
|
88
|
+
window.addEventListener('storage', onStorage);
|
|
89
|
+
return () => {
|
|
90
|
+
listeners.delete(listener);
|
|
91
|
+
media.removeEventListener('change', onSystemChange);
|
|
92
|
+
window.removeEventListener('storage', onStorage);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Applies a choice to `<html>` and stores it. The change applies even if storage is blocked. */
|
|
97
|
+
function setAppearance(choice: AppearanceChoice) {
|
|
98
|
+
try {
|
|
99
|
+
if (choice === 'system') localStorage.removeItem(APPEARANCE_STORAGE_KEY);
|
|
100
|
+
else localStorage.setItem(APPEARANCE_STORAGE_KEY, choice);
|
|
101
|
+
} catch {
|
|
102
|
+
// Storage is blocked: the mode still changes for this page.
|
|
103
|
+
}
|
|
104
|
+
stamp(choice === 'system' ? systemAppearance() : choice);
|
|
105
|
+
emit();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The page's light or dark mode. `choice` is what the person picked (`system` when they
|
|
110
|
+
* haven't); `appearance` is what is showing. On the server and in the first client render it
|
|
111
|
+
* reports `system` / `light`, then corrects without a hydration mismatch.
|
|
112
|
+
*/
|
|
113
|
+
function useAppearance() {
|
|
114
|
+
const { choice, resolved } = React.useSyncExternalStore(subscribe, getSnapshot, () => serverSnapshot);
|
|
115
|
+
return { choice, appearance: resolved, setAppearance } as const;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { useAppearance, setAppearance };
|
|
119
|
+
export type { AppearanceChoice, ResolvedAppearance };
|