@ -5,12 +5,14 @@
--_words-content-py : var ( --words-content-py-md ) ;
--_words-content-py : var ( --words-content-py-md ) ;
--_words-content-min-block-size : var ( --words-content-min-block-size-md ) ;
--_words-content-min-block-size : var ( --words-content-min-block-size-md ) ;
/ * Gutter reserved for the block-handle grip + inserter "+" overlay .
/ * Gutter reserved for the block-handle grip + inserter "+" overlay .
Content is padded by this much on the side OPPOSITE to the drawer
Rendered as a SEPARATE column outside the editor box ( its own
so the floating overlays have a visible band to land in . 1rem
background + dot pattern ) , with a small gap before the editor .
( ≈ 16px ) is the minimum : combined with the existing content-px
The grip + "+" are positioned by JS at viewport coordinates that
( 16px ) it gives a 32px band — wide enough for the 28px grip to
land inside this column . * /
start at the editor edge and leave 4px before the text . * /
--_words-rail-px : 2rem ;
--_words-rail-px : 1rem ;
--_words-rail-gap : 0 . 5rem ;
--_words-rail-bg : color-mix ( in srgb , var ( --words-toolbar-border ) 35 % , transparent ) ;
--_words-rail-dot : color-mix ( in srgb , var ( --words-status-color ) 65 % , transparent ) ;
--_words-toolbar-px : var ( --words-toolbar-px-md ) ;
--_words-toolbar-px : var ( --words-toolbar-px-md ) ;
--_words-toolbar-py : var ( --words-toolbar-py-md ) ;
--_words-toolbar-py : var ( --words-toolbar-py-md ) ;
--_words-button-size : var ( --words-button-size-md ) ;
--_words-button-size : var ( --words-button-size-md ) ;
@ -132,35 +134,62 @@
opacity : var ( --words-disabled-opacity ) ;
opacity : var ( --words-disabled-opacity ) ;
}
}
/ * ─ ─ Companion Drawer ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
/ * ─ ─ Layout ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Layout : when the editor host contains a [ data-words-drawer ] , place
The editor host is a grid : rail | gap | ( toolbar / content / status ) |
the drawer in a dedicated column that spans toolbar + content +
drawer . The rail is a dedicated column OUTSIDE the editor card that
status rows . Default side is right ; ` data-side = 'left' ` on the drawer
visually holds the block-handle grip + inserter "+" overlays — with
flips the column order . Children without a grid-area placement
its own grey background and dotted pattern , separated from the
( BubbleMenu / SlashMenu / HiddenInput / find-replace etc . ) are
editor by a small gap mirroring the drawer ' s gap on the other side .
absolute-positioned or hidden inputs that don ' t participate in the
When ` < Words . Drawer side = "left" > ` the columns flip . Children without
visual flow . * /
a grid-area placement ( BubbleMenu / SlashMenu / HiddenInput /
find-replace ) are absolute-positioned and don ' t participate . * /
[ data-words ] : has ( [ data-words-drawer ] ) {
[ data-words ] : has ( [ data-words-drawer ] ) {
display : grid ;
display : grid ;
grid-template-columns : minmax ( 0 , 1fr ) max-content ;
grid-template-columns :
var ( --_words-rail-px )
var ( --_words-rail-gap )
minmax ( 0 , 1fr )
max-content ;
grid-template-rows : auto minmax ( 0 , 1fr ) auto ;
grid-template-rows : auto minmax ( 0 , 1fr ) auto ;
grid-template-areas :
grid-template-areas :
'toolbar drawer'
' rail . toolbar drawer'
'content drawer'
' rail . content drawer'
' status drawer';
' rail . status drawer';
column-gap : 0 ;
column-gap : 0 ;
}
}
/ * Left-side drawer — flip the column order so the drawer takes the
/ * Left-side drawer — drawer first , then rail at the END mirroring
start column and the editor flow takes the rest . The drawer ' s
the right-side layout ( rail is on the SAME side as the grip ,
own border-inline-start ( toward the content ) is what visually
opposite to the drawer ) . * /
separates them ; we swap it to border-inline-end below . * /
[ data-words ] : has ( [ data-words-drawer ] [ data-side = 'left' ] ) {
[ data-words ] : has ( [ data-words-drawer ] [ data-side = 'left' ] ) {
grid-template-columns : max-content minmax ( 0 , 1fr ) ;
grid-template-columns :
max-content
minmax ( 0 , 1fr )
var ( --_words-rail-gap )
var ( --_words-rail-px ) ;
grid-template-areas :
grid-template-areas :
'drawer toolbar'
'drawer toolbar . rail'
'drawer content'
'drawer content . rail'
'drawer status' ;
'drawer status . rail' ;
}
/ * Rail visual — pseudo-element on the editor host that fills the
` rail ` grid area with the dotted grey band . The grip + "+" overlays
are JS-positioned via viewport coordinates that land here . * /
[ data-words ] : has ( [ data-words-drawer ] ) :: before {
content : '' ;
grid-area : rail ;
background-color : var ( --_words-rail-bg ) ;
background-image : radial-gradient (
circle 1px ,
var ( --_words-rail-dot ) 99 % ,
transparent 100 %
) ;
background-size : 5px 5px ;
background-position : 50 % 50 % ;
background-repeat : repeat ;
border-radius : var ( --_words-radius ) ;
pointer-events : none ;
}
}
[ data-words ] : has ( [ data-words-drawer ] ) > [ data-words-toolbar ] {
[ data-words ] : has ( [ data-words-drawer ] ) > [ data-words-toolbar ] {
@ -769,9 +798,10 @@
border : 0 ;
border : 0 ;
border-radius : var ( --words-command-radius ) ;
border-radius : var ( --words-command-radius ) ;
background : transparent ;
background : transparent ;
color : var ( --words-status-color ) ;
/* Distinct color against the dotted rail. Accent on hover. */
color : var ( --words-command-color ) ;
cursor : grab ;
cursor : grab ;
opacity : 0 . 4 5;
opacity : 0 . 7 5;
transition : opacity 120ms ease , background-color 120ms ease , color 120ms ease ;
transition : opacity 120ms ease , background-color 120ms ease , color 120ms ease ;
z-index : var ( --z-index-popover ) ;
z-index : var ( --z-index-popover ) ;
}
}
@ -779,8 +809,9 @@
[ data-words-block-handle ] : hover ,
[ data-words-block-handle ] : hover ,
[ data-words-block-handle ] [ data-open ] {
[ data-words-block-handle ] [ data-open ] {
opacity : 1 ;
opacity : 1 ;
background : var ( --words-command-hover-bg ) ;
background : var ( --words-toolbar-bg ) ;
color : var ( --words-command-active-color ) ;
color : var ( --_words-accent-text ) ;
box-shadow : 0 0 0 var ( --words-border-width ) var ( --_words-accent-border ) ;
}
}
[ data-words-block-handle ] : active {
[ data-words-block-handle ] : active {
@ -853,7 +884,14 @@
[ data-words-block-inserter-button ] {
[ data-words-block-inserter-button ] {
pointer-events : auto ;
pointer-events : auto ;
position : absolute ;
position : absolute ;
left : -28px ;
/ * Land the "+" centered in the dedicated rail column to the left
of the editor box . Same metrics as the block-handle ' s positioning :
block . rect . left − content-px − rail-gap − rail-px / 2 − 12 ( button half ) .
The inserter wrapper sits at block . rect . left , so we offset by
− ( content-px + rail-gap + rail-px / 2 + 12 ) . * /
left : calc (
-1 * ( var ( --_words-content-px ) + var ( --_words-rail-gap ) + var ( --_words-rail-px ) / 2 ) - 12px
) ;
display : inline-flex ;
display : inline-flex ;
align-items : center ;
align-items : center ;
justify-content : center ;
justify-content : center ;
@ -878,12 +916,14 @@
color : var ( --_words-accent-on ) ;
color : var ( --_words-accent-on ) ;
}
}
/ * When the drawer is on the LEFT side , flip the "+" button to the
/ * When the drawer is on the LEFT side , flip the "+" button so it
block's right margin so it doesn' t clash with the drawer ' s
lands in the rail column on the editor ' s RIGHT side . Same metric
gutter ( mirrors the block-handle grip behaviour ) . * /
math as the right-drawer case , just on the inline-end side . * /
[ data-words-block-inserter ] [ data-side = 'left' ] [ data-words-block-inserter-button ] {
[ data-words-block-inserter ] [ data-side = 'left' ] [ data-words-block-inserter-button ] {
left : auto ;
left : auto ;
right : -28px ;
right : calc (
-1 * ( var ( --_words-content-px ) + var ( --_words-rail-gap ) + var ( --_words-rail-px ) / 2 ) - 12px
) ;
}
}
[ data-words-block-inserter-line ] {
[ data-words-block-inserter-line ] {
@ -1766,9 +1806,7 @@
[ data-words-placeholder ] {
[ data-words-placeholder ] {
display : block ;
display : block ;
padding-block-start : var ( --_words-content-py ) ;
padding-block-start : var ( --_words-content-py ) ;
/* Default (drawer on right): rail on the LEFT for the grip. */
padding-inline : var ( --_words-content-px ) ;
padding-inline-start : calc ( var ( --_words-content-px ) + var ( --_words-rail-px ) ) ;
padding-inline-end : var ( --_words-content-px ) ;
color : var ( --words-placeholder-color ) ;
color : var ( --words-placeholder-color ) ;
pointer-events : none ;
pointer-events : none ;
}
}
@ -1779,12 +1817,9 @@
}
}
[ data-words-content ] {
[ data-words-content ] {
position : relative ;
min-block-size : var ( --_words-content-min-block-size ) ;
min-block-size : var ( --_words-content-min-block-size ) ;
padding-block : var ( --_words-content-py ) ;
padding-block : var ( --_words-content-py ) ;
/* Default (drawer on right): rail on the LEFT for the grip + "+". */
padding-inline : var ( --_words-content-px ) ;
padding-inline-start : calc ( var ( --_words-content-px ) + var ( --_words-rail-px ) ) ;
padding-inline-end : var ( --_words-content-px ) ;
color : var ( --words-content-color ) ;
color : var ( --words-content-color ) ;
outline : none ;
outline : none ;
overflow-wrap : anywhere ;
overflow-wrap : anywhere ;
@ -1794,42 +1829,6 @@
-webkit-user-select : text ;
-webkit-user-select : text ;
}
}
/ * Dotted band — fills the rail ' s padding zone so the user reads it as
a dedicated gutter ( where grip + "+" overlays live ) instead of empty
content padding . Pointer-events disabled so it doesn ' t intercept
clicks / selection . Pseudo-elements on a contenteditable don ' t affect
text selection or caret in modern browsers . * /
[ data-words-content ] :: before {
content : '' ;
position : absolute ;
inset-block : 0 ;
inset-inline-start : 0 ;
inline-size : calc ( var ( --_words-rail-px ) + var ( --_words-content-px ) ) ;
background-image : radial-gradient (
circle 1px ,
color-mix ( in srgb , var ( --words-status-color ) 40 % , transparent ) 99 % ,
transparent 100 %
) ;
background-size : 8px 8px ;
background-position : 50 % 50 % ;
background-repeat : repeat ;
pointer-events : none ;
z-index : 0 ;
}
[ data-words ] : has ( [ data-words-drawer ] [ data-side = 'left' ] ) > [ data-words-content ] :: before {
inset-inline-start : auto ;
inset-inline-end : 0 ;
}
/ * When the drawer lives on the LEFT , mirror : rail moves to the RIGHT
so the grip + "+" don 't fight the drawer' s gutter . * /
[ data-words ] : has ( [ data-words-drawer ] [ data-side = 'left' ] ) > [ data-words-content ] ,
[ data-words ] : has ( [ data-words-drawer ] [ data-side = 'left' ] ) > [ data-words-placeholder ] {
padding-inline-start : var ( --_words-content-px ) ;
padding-inline-end : calc ( var ( --_words-content-px ) + var ( --_words-rail-px ) ) ;
}
[ data-words-content ] * {
[ data-words-content ] * {
user-select : text ;
user-select : text ;
-webkit-user-select : text ;
-webkit-user-select : text ;