mnfst 0.5.194 → 0.5.196

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/lib/manifest.css CHANGED
@@ -274,13 +274,16 @@
274
274
  color: color-mix(in oklab, currentcolor 50%, transparent)
275
275
  }
276
276
 
277
+ /* Animates `scale`, not `transform`: a transform animation with fill `both`
278
+ keeps its final value and would override any transform an author sets on
279
+ a popover of their own. */
277
280
  @keyframes mnfst-popover-in {
278
281
  from {
279
- transform: scale(.9);
282
+ scale: .9;
280
283
  }
281
284
 
282
285
  to {
283
- transform: none;
286
+ scale: 1;
284
287
  }
285
288
  }
286
289
 
@@ -2547,8 +2550,11 @@
2547
2550
  /* Hide dropdowns when dialogs are open */
2548
2551
  html:has(dialog:popover-open) {
2549
2552
 
2550
- /* Hide dropdowns outside of any dialog */
2551
- & menu[popover]:not(dialog *) {
2553
+ /* Hide dropdowns outside of any dialog. A generated popover (a datepicker
2554
+ calendar, a colorpicker panel) is appended to <body>, so DOM position alone
2555
+ can't tell page chrome from a menu the dialog itself opened —
2556
+ data-popover-nested marks the latter, which CSS has no way to work out. */
2557
+ & menu[popover]:not(dialog *):not([data-popover-nested]) {
2552
2558
  opacity: 0;
2553
2559
  scale: .9;
2554
2560
  pointer-events: none;
@@ -2893,21 +2899,36 @@
2893
2899
 
2894
2900
  @layer base, components, utilities;
2895
2901
 
2896
- /* Authors override any of this without specificity battles: every rule is wrapped
2897
- in :where(), state lives on data-attributes, and injected affordances use plain
2898
- semantic class names (.edit-handle, .edit-frame) — no framework-prefixed litter. */
2902
+ /* Self-contained: every rule the plugin needs lives here, so this stylesheet works
2903
+ on its own. Authors override without specificity battles — rules are :where()
2904
+ wrapped and keyed off data-edit-* attributes, including the injected affordances
2905
+ (handles, overlay, live region, toolbar, menu). No class names to collide with. */
2899
2906
 
2900
2907
  @layer utilities {
2901
2908
 
2902
- /* ---- Editable area (armed) ---- */
2909
+ /* ---- Editable area (armed) ----
2910
+ Chrome is opt-in. Most uses of this plugin are a resize handle or a sortable
2911
+ list, which should look like themselves; only .authoring gets the outline and
2912
+ the region label. */
2903
2913
  :where([data-edit-armed]) {
2904
2914
  position: relative;
2905
- outline: 1px dashed var(--color-line);
2915
+ }
2916
+
2917
+ /* Quiet until touched. A page covered in dashed boxes reads as a diagram of
2918
+ itself; the affordance only has to be there when you go looking for it. */
2919
+ :where([data-edit-armed][data-edit-authoring]) {
2920
+ outline: 1px dashed transparent;
2906
2921
  outline-offset: 6px;
2907
2922
  border-radius: var(--radius);
2923
+ transition: outline-color 0.12s ease;
2924
+ }
2925
+
2926
+ :where([data-edit-armed][data-edit-authoring]):hover,
2927
+ :where([data-edit-armed][data-edit-authoring]):focus-within {
2928
+ outline-color: var(--color-line);
2908
2929
  }
2909
2930
 
2910
- :where([data-edit-armed])::before {
2931
+ :where([data-edit-armed][data-edit-authoring])::before {
2911
2932
  content: attr(data-edit-label);
2912
2933
  position: absolute;
2913
2934
  inset-block-start: -1.5rem;
@@ -2917,6 +2938,13 @@
2917
2938
  text-transform: uppercase;
2918
2939
  color: var(--color-content-subtle);
2919
2940
  pointer-events: none;
2941
+ opacity: 0;
2942
+ transition: opacity 0.12s ease;
2943
+ }
2944
+
2945
+ :where([data-edit-armed][data-edit-authoring]):hover::before,
2946
+ :where([data-edit-armed][data-edit-authoring]):focus-within::before {
2947
+ opacity: 1;
2920
2948
  }
2921
2949
 
2922
2950
  /* ---- Reorder (pointer + keyboard; touch-action:none lets touch-drag beat scroll) ---- */
@@ -2925,31 +2953,55 @@
2925
2953
  touch-action: none;
2926
2954
  transition: opacity 0.12s ease, outline-color 0.12s ease;
2927
2955
  }
2928
- :where([data-edit-sortable]):hover {
2956
+
2957
+ :where([data-edit-sortable]):hover,
2958
+ :where([data-edit-sortable]):focus-within {
2929
2959
  outline: 1px solid var(--edit-accent, var(--color-brand-content));
2930
2960
  outline-offset: 3px;
2931
2961
  }
2932
- :where([data-edit-sortable]):focus-visible,
2933
- :where([data-edit-movable]):focus-visible {
2962
+
2963
+ :where([data-edit-sortable], [data-edit-movable]):focus-visible {
2934
2964
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
2935
2965
  outline-offset: 3px;
2936
2966
  }
2937
- :where([data-edit-dragging]) { opacity: 0.4; }
2967
+
2968
+ /* Lifted out of the flow and following the pointer. */
2969
+ :where([data-edit-sortable][data-edit-dragging]) {
2970
+ z-index: 1000;
2971
+ cursor: grabbing;
2972
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.18);
2973
+ }
2974
+
2975
+ /* The stand-in holding the slot the drop would land in — a translucent copy of
2976
+ the element by default, because the clearest preview of where it lands is the
2977
+ thing itself. Restyle it freely: hide its contents for an empty outlined slot,
2978
+ give it a background, animate it. */
2979
+ :where([data-edit-ghost]) {
2980
+ opacity: var(--edit-ghost-opacity, 0.4);
2981
+ pointer-events: none;
2982
+ }
2983
+
2984
+ /* Nothing else should react while a drag is in progress */
2985
+ :where([data-edit-dragging-in]) {
2986
+ cursor: grabbing;
2987
+ user-select: none;
2988
+
2989
+ & :where([data-edit-sortable]:not([data-edit-dragging])) {
2990
+ cursor: grabbing;
2991
+ }
2992
+ }
2993
+
2938
2994
  :where([data-edit-grabbed]) {
2939
2995
  outline: 2px dashed var(--edit-accent, var(--color-brand-content));
2940
2996
  outline-offset: 3px;
2941
2997
  }
2942
2998
 
2943
- /* Screen-reader announcement region (keyboard grab/move/drop) */
2944
- :where(.edit-sr) {
2945
- position: absolute;
2946
- width: 1px; height: 1px;
2947
- margin: -1px; padding: 0; border: 0;
2948
- overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
2999
+ /* ---- Move (positioned children) ---- */
3000
+ :where([data-edit-movable]) {
3001
+ cursor: move;
3002
+ touch-action: none;
2949
3003
  }
2950
3004
 
2951
- /* ---- Move (positioned children) ---- */
2952
- :where([data-edit-movable]) { cursor: move; touch-action: none; }
2953
3005
  :where([data-edit-movable]):hover {
2954
3006
  outline: 1px solid var(--edit-accent, var(--color-brand-content));
2955
3007
  outline-offset: 2px;
@@ -2957,83 +3009,204 @@
2957
3009
 
2958
3010
  /* ---- Inline text ---- */
2959
3011
  :where([data-edit-armed]) [contenteditable="true"] {
2960
- outline: 1px solid color-mix(in srgb, var(--edit-accent, var(--color-brand-content)) 45%, transparent);
3012
+ outline: 1px solid transparent;
2961
3013
  outline-offset: 2px;
2962
3014
  border-radius: 3px;
3015
+ transition: outline-color 0.12s ease;
3016
+ }
3017
+
3018
+ :where([data-edit-armed]) [contenteditable="true"]:hover {
3019
+ outline-color: color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 45%, transparent);
2963
3020
  }
3021
+
2964
3022
  :where([data-edit-armed]) [contenteditable="true"]:focus {
2965
3023
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
2966
3024
  }
2967
3025
 
2968
- /* ---- Size (resize handles) — injected overlay children, fully stylable ---- */
2969
- /* The element keeps its own markup untouched, so its native :hover/:focus still
2970
- fire. Handles are separate elements with pointer-events; the body of the
2971
- element stays interactive. Geometry: styles/snippets/manifest.handles.css. */
3026
+ /* ---- Size handles ----
3027
+ Injected as siblings of the element's own markup, so its native :hover and
3028
+ :focus still fire and its body stays interactive. */
2972
3029
  :where([data-edit-sizable]) {
2973
3030
  position: relative;
3031
+ }
3032
+
3033
+ /* Handles reveal on approach rather than sitting on the element permanently */
3034
+ :where([data-edit-sizable]) :where([data-edit-handle])::before {
3035
+ opacity: 0;
3036
+ transition: opacity 0.12s ease, background-color 0.15s ease;
3037
+ }
2974
3038
 
2975
- .edit-handle {
3039
+ :where([data-edit-sizable]):hover :where([data-edit-handle])::before,
3040
+ :where([data-edit-sizable]):focus-within :where([data-edit-handle])::before {
3041
+ opacity: 1;
3042
+ }
3043
+
3044
+ :where([data-edit-handle]) {
3045
+ position: absolute;
3046
+ z-index: 100;
3047
+ background: transparent;
3048
+ --h: var(--edit-size-handle, 1rem);
3049
+
3050
+ /* Hairline, revealed on hover */
3051
+ &::before {
3052
+ content: '';
2976
3053
  position: absolute;
2977
- z-index: 100;
3054
+ top: 50%;
3055
+ left: 50%;
3056
+ transform: translate(-50%, -50%);
3057
+ width: 1px;
3058
+ height: 1px;
2978
3059
  background: transparent;
2979
- --h: var(--edit-size-handle, 1rem);
3060
+ transition: background-color 0.15s ease;
3061
+ pointer-events: none;
3062
+ }
2980
3063
 
2981
- &::before {
2982
- content: '';
2983
- position: absolute;
2984
- top: 50%;
2985
- left: 50%;
2986
- transform: translate(-50%, -50%);
2987
- width: 1px;
2988
- height: 1px;
2989
- background: transparent;
2990
- transition: background-color 0.15s ease;
2991
- }
2992
- &:hover::before, &:active::before {
2993
- background-color: var(--edit-accent, var(--color-brand-content));
2994
- }
3064
+ &:hover::before,
3065
+ &:active::before {
3066
+ background-color: var(--edit-accent, var(--color-brand-content));
2995
3067
  }
3068
+ }
2996
3069
 
2997
- /* Corner cursors (geometry is shared; resize handles use nw/ne/sw/se) */
2998
- .edit-handle-top-left, .edit-handle-bottom-right,
2999
- .edit-handle-top-start, .edit-handle-bottom-end { cursor: nwse-resize; }
3000
- .edit-handle-top-right, .edit-handle-bottom-left,
3001
- .edit-handle-top-end, .edit-handle-bottom-start { cursor: nesw-resize; }
3070
+ :where([data-edit-handle]):focus-visible {
3071
+ outline: 2px solid var(--edit-accent, var(--color-brand-content));
3002
3072
  }
3003
3073
 
3004
- /* Collapsed state (size dropped below --edit-size-collapse-*) — authors restyle freely */
3005
- :where([data-edit-collapsed]) { opacity: 0.45; }
3074
+ /* Edges span their axis; corners are square. A corner value contains a hyphen. */
3075
+ :where([data-edit-handle="top"], [data-edit-handle="bottom"]) {
3076
+ left: 0;
3077
+ width: 100%;
3078
+ height: var(--h);
3079
+ cursor: ns-resize;
3006
3080
 
3007
- /* Full-viewport capture layer during a size/move drag (mirrors resize overlay) */
3008
- :where(.edit-overlay) {
3081
+ &::before {
3082
+ width: 100%;
3083
+ }
3084
+ }
3085
+
3086
+ :where([data-edit-handle="left"], [data-edit-handle="right"], [data-edit-handle="start"], [data-edit-handle="end"]) {
3087
+ top: 0;
3088
+ width: var(--h);
3089
+ height: 100%;
3090
+ cursor: ew-resize;
3091
+
3092
+ &::before {
3093
+ height: 100%;
3094
+ }
3095
+ }
3096
+
3097
+ :where([data-edit-handle*="-"]) {
3098
+ width: var(--h);
3099
+ height: var(--h);
3100
+ }
3101
+
3102
+ /* Inset — each handle straddles its edge; a corner matches one rule per axis.
3103
+ inset-inline-* carries the logical start/end values, so they flip for RTL. */
3104
+ :where([data-edit-handle^="top"]) {
3105
+ top: calc(var(--h) * -0.5);
3106
+ }
3107
+
3108
+ :where([data-edit-handle^="bottom"]) {
3109
+ bottom: calc(var(--h) * -0.5);
3110
+ }
3111
+
3112
+ :where([data-edit-handle$="left"]) {
3113
+ left: calc(var(--h) * -0.5);
3114
+ }
3115
+
3116
+ :where([data-edit-handle$="right"]) {
3117
+ right: calc(var(--h) * -0.5);
3118
+ }
3119
+
3120
+ :where([data-edit-handle$="start"]) {
3121
+ inset-inline-start: calc(var(--h) * -0.5);
3122
+ }
3123
+
3124
+ :where([data-edit-handle$="end"]) {
3125
+ inset-inline-end: calc(var(--h) * -0.5);
3126
+ }
3127
+
3128
+ /* Corner cursors; the logical pair flips with direction. */
3129
+ :where([data-edit-handle="top-left"], [data-edit-handle="bottom-right"], [data-edit-handle="top-start"], [data-edit-handle="bottom-end"]) {
3130
+ cursor: nwse-resize;
3131
+ }
3132
+
3133
+ :where([data-edit-handle="top-right"], [data-edit-handle="bottom-left"], [data-edit-handle="top-end"], [data-edit-handle="bottom-start"]) {
3134
+ cursor: nesw-resize;
3135
+ }
3136
+
3137
+ [dir=rtl] :where([data-edit-handle="top-start"], [data-edit-handle="bottom-end"]) {
3138
+ cursor: nesw-resize;
3139
+ }
3140
+
3141
+ [dir=rtl] :where([data-edit-handle="top-end"], [data-edit-handle="bottom-start"]) {
3142
+ cursor: nwse-resize;
3143
+ }
3144
+
3145
+ /* Collapsed past --edit-size-collapse-* — authors restyle freely */
3146
+ :where([data-edit-collapsed]) {
3147
+ opacity: 0.45;
3148
+ }
3149
+
3150
+ /* Full-viewport capture layer during a size or move drag */
3151
+ :where([data-edit-overlay]) {
3009
3152
  position: fixed;
3010
3153
  inset: 0;
3011
3154
  z-index: 9999;
3012
3155
  background: transparent;
3013
3156
  }
3014
3157
 
3158
+ /* Screen-reader announcements (keyboard grab / move / drop) */
3159
+ :where([data-edit-live]) {
3160
+ position: absolute;
3161
+ width: 1px;
3162
+ height: 1px;
3163
+ margin: -1px;
3164
+ padding: 0;
3165
+ border: 0;
3166
+ overflow: hidden;
3167
+ clip-path: inset(50%);
3168
+ white-space: nowrap;
3169
+ }
3170
+
3015
3171
  @media (prefers-reduced-motion: reduce) {
3016
- :where([data-edit-armed]) > [draggable="true"], :where([data-edit-sizable]) .edit-handle::before { transition: none; }
3172
+
3173
+ :where([data-edit-armed]) > [draggable="true"],
3174
+ :where([data-edit-handle])::before {
3175
+ transition: none;
3176
+ }
3017
3177
  }
3018
3178
  }
3019
3179
 
3020
- /* ---- Plugin dev chrome (spike-only: toolbar + class/scope menu) ---- */
3180
+ /* ---- Authoring chrome (toolbar + class/scope menu) ---- */
3021
3181
  @layer components {
3022
3182
 
3023
- :where(.edit-toolbar) {
3183
+ /* Presence is script's business — it knows which route is on screen. Whether it
3184
+ is seen is not, so the toolbar is styled and revealed here. */
3185
+ :where([data-edit-toolbar]) {
3186
+ display: none;
3024
3187
  position: fixed;
3025
3188
  inset-block-end: 1rem;
3026
3189
  inset-inline-end: 1rem;
3027
3190
  z-index: 9999;
3028
- display: flex;
3029
3191
  gap: 0.5rem;
3030
3192
  align-items: center;
3193
+
3194
+ & button[disabled] {
3195
+ opacity: 0.35;
3196
+ pointer-events: none;
3197
+ }
3031
3198
  }
3032
3199
 
3033
- :where(.edit-classes) {
3200
+ :root[data-edit-active] :where([data-edit-toolbar]) {
3201
+ display: flex;
3202
+ }
3203
+
3204
+ /* A popover: the top layer sits above every stacking context, so no handle,
3205
+ overlay or z-index anywhere on the page can paint over it. */
3206
+ :where([data-edit-menu]) {
3034
3207
  position: fixed;
3035
- z-index: 10000;
3036
- width: 260px;
3208
+ margin: 0;
3209
+ width: 16rem;
3037
3210
  padding: 0.6rem;
3038
3211
  display: flex;
3039
3212
  flex-direction: column;
@@ -3042,12 +3215,20 @@
3042
3215
  border: 1px solid var(--color-line);
3043
3216
  border-radius: var(--radius);
3044
3217
  box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
3045
- }
3046
- :where(.edit-classes)[hidden] { display: none; }
3047
- :where(.edit-classes) input { width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.75rem; }
3048
- :where(.edit-classes) .muted { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-content-subtle); }
3049
3218
 
3050
- :where(.edit-toolbar) button[disabled] { opacity: 0.35; pointer-events: none; }
3219
+ & input {
3220
+ width: 100%;
3221
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
3222
+ font-size: 0.75rem;
3223
+ }
3224
+
3225
+ & small {
3226
+ font-size: 0.7rem;
3227
+ text-transform: uppercase;
3228
+ letter-spacing: 0.05em;
3229
+ color: var(--color-content-subtle);
3230
+ }
3231
+ }
3051
3232
  }
3052
3233
 
3053
3234
  /* Manifest Forms */
@@ -3087,8 +3268,10 @@
3087
3268
 
3088
3269
  &>:where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]) {
3089
3270
 
3090
- /* First participant */
3091
- &:not(:where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]) ~ &) {
3271
+ /* First participant — only when something follows it. A lone
3272
+ participant matched this AND the last rule, squaring all four
3273
+ corners of what is really just a single field. */
3274
+ &:not(:where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]) ~ &):has(~ :where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"])) {
3092
3275
  border-start-end-radius: 0;
3093
3276
  border-end-end-radius: 0
3094
3277
  }
@@ -3098,8 +3281,8 @@
3098
3281
  border-radius: 0
3099
3282
  }
3100
3283
 
3101
- /* Last participant */
3102
- &:not(:has(~ :where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]))) {
3284
+ /* Last participant — only when something precedes it */
3285
+ &:where(:where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]) ~ &):not(:has(~ :where(button, [role=button], input:not([type=hidden]), select, textarea, .combobox, label:has(input:not([type=checkbox], [type=radio]), select, textarea), .label:has(input:not([type=checkbox], [type=radio]), select, textarea)):not([hidden]):not([popover]):not(.hidden):not([style*="display: none"]))) {
3103
3286
  border-start-start-radius: 0;
3104
3287
  border-end-start-radius: 0
3105
3288
  }
@@ -3707,7 +3890,10 @@
3707
3890
  }
3708
3891
  }
3709
3892
 
3710
- /* Manifest Resize Handles */
3893
+ /* Manifest Resize Handles
3894
+
3895
+ Superseded by x-edit.size (manifest.edit.css) — kept for existing projects.
3896
+ Self-contained: handle geometry lives here, not in a shared snippet. */
3711
3897
 
3712
3898
  @layer base, components, utilities;
3713
3899
 
@@ -3716,7 +3902,7 @@
3716
3902
  :where([x-resize]) {
3717
3903
  position: relative;
3718
3904
 
3719
- /* Resize handle area (geometry: styles/snippets/manifest.handles.css) */
3905
+ /* Resize handle area */
3720
3906
  .resize-handle {
3721
3907
  position: absolute;
3722
3908
  display: block;
@@ -3748,7 +3934,81 @@
3748
3934
  }
3749
3935
  }
3750
3936
 
3751
- /* Corner cursors (geometry is shared; edit handles use nwse/nesw) */
3937
+ /* Handle geometry — edges span their axis, corners stay square */
3938
+ .resize-handle-top,
3939
+ .resize-handle-bottom {
3940
+ left: 0;
3941
+ width: 100%;
3942
+ height: var(--h);
3943
+ cursor: ns-resize;
3944
+
3945
+ &::before {
3946
+ width: 100%
3947
+ }
3948
+ }
3949
+
3950
+ .resize-handle-left,
3951
+ .resize-handle-right,
3952
+ .resize-handle-start,
3953
+ .resize-handle-end {
3954
+ top: 0;
3955
+ width: var(--h);
3956
+ height: 100%;
3957
+ cursor: ew-resize;
3958
+
3959
+ &::before {
3960
+ height: 100%
3961
+ }
3962
+ }
3963
+
3964
+ .resize-handle-top {
3965
+ top: calc(var(--h) * -0.5)
3966
+ }
3967
+
3968
+ .resize-handle-bottom {
3969
+ bottom: calc(var(--h) * -0.5)
3970
+ }
3971
+
3972
+ .resize-handle-left,
3973
+ .resize-handle-top-left,
3974
+ .resize-handle-bottom-left {
3975
+ left: calc(var(--h) * -0.5)
3976
+ }
3977
+
3978
+ .resize-handle-right,
3979
+ .resize-handle-top-right,
3980
+ .resize-handle-bottom-right {
3981
+ right: calc(var(--h) * -0.5)
3982
+ }
3983
+
3984
+ /* Logical edges — inset-inline-* auto-flips for RTL */
3985
+ .resize-handle-start,
3986
+ .resize-handle-top-start,
3987
+ .resize-handle-bottom-start {
3988
+ inset-inline-start: calc(var(--h) * -0.5)
3989
+ }
3990
+
3991
+ .resize-handle-end,
3992
+ .resize-handle-top-end,
3993
+ .resize-handle-bottom-end {
3994
+ inset-inline-end: calc(var(--h) * -0.5)
3995
+ }
3996
+
3997
+ .resize-handle-top-left,
3998
+ .resize-handle-top-right,
3999
+ .resize-handle-top-start,
4000
+ .resize-handle-top-end {
4001
+ top: calc(var(--h) * -0.5)
4002
+ }
4003
+
4004
+ .resize-handle-bottom-left,
4005
+ .resize-handle-bottom-right,
4006
+ .resize-handle-bottom-start,
4007
+ .resize-handle-bottom-end {
4008
+ bottom: calc(var(--h) * -0.5)
4009
+ }
4010
+
4011
+ /* Corner cursors */
3752
4012
  .resize-handle-top-left,
3753
4013
  .resize-handle-top-start {
3754
4014
  cursor: nw-resize;
@@ -4121,6 +4381,211 @@
4121
4381
  }
4122
4382
  }
4123
4383
 
4384
+ /* Manifest Text Edit */
4385
+
4386
+ @layer base, components, utilities;
4387
+
4388
+ /* Self-contained. The editable area is a field first — it borrows the tokens inputs
4389
+ and textareas use, so it sits in a form without looking bolted on. Controls are
4390
+ styled where they are, not by where they sit, because they can be anywhere. */
4391
+
4392
+ @layer components {
4393
+
4394
+ :where([data-text-edit]) {
4395
+ min-height: var(--text-edit-min-height, 8rem);
4396
+ max-height: var(--text-edit-max-height, none);
4397
+ overflow-y: auto;
4398
+ padding: var(--text-edit-padding, 0.75rem);
4399
+ background: var(--text-edit-background, var(--color-surface-1));
4400
+ border: 1px solid var(--color-line);
4401
+ border-radius: var(--radius);
4402
+ outline: none;
4403
+
4404
+ /* Page-level styling — set by .page controls, read back by $text.page.
4405
+ Presentation, so it lives here rather than inside the stored markup. */
4406
+ font-family: var(--text-edit-font, inherit);
4407
+ font-size: var(--text-edit-size, inherit);
4408
+ line-height: var(--text-edit-leading, inherit);
4409
+ text-align: var(--text-edit-align, start);
4410
+ color: var(--text-edit-color, inherit);
4411
+
4412
+ &:focus {
4413
+ border-color: var(--color-content-subtle);
4414
+ }
4415
+
4416
+ /* Flagged from script: :empty never matches the <br> a contenteditable
4417
+ keeps, and the tag holding it is not predictable after a wipe. */
4418
+ &[data-text-edit-empty]::before {
4419
+ content: attr(placeholder);
4420
+ color: var(--color-content-subtle);
4421
+ pointer-events: none;
4422
+ }
4423
+ }
4424
+
4425
+ /* ---- Controls ----
4426
+ A control is marked wherever it lives, so a toolbar is optional markup and
4427
+ never a requirement. The reset already dims [aria-disabled=true]. */
4428
+ :where([data-text-edit-control]) {
4429
+ &[data-text-edit-active] {
4430
+ background: color-mix(in oklch, var(--color-content-stark) 10%, transparent);
4431
+ color: var(--color-content-stark);
4432
+ }
4433
+
4434
+ & > svg {
4435
+ width: 1rem;
4436
+ height: 1rem;
4437
+ }
4438
+ }
4439
+
4440
+ :where(button[data-text-edit-control]) {
4441
+ padding: 0.35rem;
4442
+ line-height: 0;
4443
+ }
4444
+
4445
+ /* ---- Editable content ----
4446
+ The editor authors real block elements, so style them as elements. Deliberately
4447
+ lighter than the .prose class, which sets a reading measure this field can't use. */
4448
+ :where([data-text-edit]) {
4449
+
4450
+ & > * + * {
4451
+ margin-block-start: 0.75em;
4452
+ }
4453
+
4454
+ & :where(h1, h2, h3, h4, h5, h6) {
4455
+ font-weight: 600;
4456
+ line-height: 1.25;
4457
+ }
4458
+
4459
+ & h1 { font-size: 1.6em }
4460
+ & h2 { font-size: 1.35em }
4461
+ & h3 { font-size: 1.15em }
4462
+
4463
+ & :where(ul, ol) {
4464
+ padding-inline-start: 1.5em;
4465
+ }
4466
+
4467
+ & li {
4468
+ list-style: revert;
4469
+ }
4470
+
4471
+ & blockquote {
4472
+ padding-inline-start: 0.85em;
4473
+ border-inline-start: 2px solid var(--color-line);
4474
+ color: var(--color-content-neutral);
4475
+ }
4476
+
4477
+ & code {
4478
+ padding: 0.1em 0.3em;
4479
+ border-radius: 4px;
4480
+ font-size: 0.9em;
4481
+ background: color-mix(in oklch, var(--color-content-stark) 8%, transparent);
4482
+ }
4483
+
4484
+ & pre {
4485
+ padding: 0.75em;
4486
+ border-radius: var(--radius);
4487
+ overflow-x: auto;
4488
+ background: color-mix(in oklch, var(--color-content-stark) 8%, transparent);
4489
+
4490
+ & code {
4491
+ padding: 0;
4492
+ background: none;
4493
+ }
4494
+ }
4495
+
4496
+ & a {
4497
+ color: var(--color-brand-content);
4498
+ text-decoration: underline;
4499
+ }
4500
+
4501
+ & hr {
4502
+ margin-block: 1em;
4503
+ }
4504
+
4505
+ & img {
4506
+ max-width: 100%;
4507
+ }
4508
+
4509
+ & table {
4510
+ width: 100%;
4511
+ table-layout: fixed;
4512
+
4513
+ & :where(td, th) {
4514
+ padding: 0.35em 0.5em;
4515
+ border: 1px solid var(--color-line);
4516
+ vertical-align: top;
4517
+ }
4518
+
4519
+ & th {
4520
+ font-weight: 600;
4521
+ text-align: start;
4522
+ background: var(--color-surface-2);
4523
+ }
4524
+ }
4525
+
4526
+ /* A block selection across cells. The browser can only highlight text, so an
4527
+ empty cell shows nothing — this is what makes a spreadsheet-style
4528
+ selection visible. */
4529
+ & :where(td, th)[data-text-edit-cell] {
4530
+ background: color-mix(in oklch, var(--color-brand-content) 18%, transparent);
4531
+ }
4532
+
4533
+ /* Borders read as grabbable only when the pointer is on one */
4534
+ &[data-text-edit-border="x"] { cursor: col-resize }
4535
+ &[data-text-edit-border="y"] { cursor: row-resize }
4536
+ &[data-text-edit-resizing] { user-select: none }
4537
+ &[data-text-edit-resizing="x"] { cursor: col-resize }
4538
+ &[data-text-edit-resizing="y"] { cursor: row-resize }
4539
+
4540
+ /* Task lists — the checkbox replaces the marker */
4541
+ & :where(ul[data-checklist]) {
4542
+ padding-inline-start: 0.25em;
4543
+ list-style: none;
4544
+
4545
+ & li {
4546
+ display: flex;
4547
+ align-items: baseline;
4548
+ gap: 0.5em;
4549
+ list-style: none;
4550
+ }
4551
+ }
4552
+
4553
+ & :where(dt) {
4554
+ font-weight: 600;
4555
+ }
4556
+
4557
+ & :where(dd) {
4558
+ margin-inline-start: 1.5em;
4559
+ }
4560
+
4561
+ & :where(kbd) {
4562
+ padding: 0.1em 0.35em;
4563
+ border: 1px solid var(--color-line);
4564
+ border-radius: 4px;
4565
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
4566
+ font-size: 0.85em;
4567
+ }
4568
+
4569
+ & :where(mark) {
4570
+ background: color-mix(in oklch, var(--color-brand-content) 25%, transparent);
4571
+ color: inherit;
4572
+ }
4573
+
4574
+ & :where(small) {
4575
+ font-size: 0.8em;
4576
+ }
4577
+
4578
+ & :where(figcaption) {
4579
+ font-size: 0.8em;
4580
+ color: var(--color-content-neutral);
4581
+ }
4582
+
4583
+ & :where(abbr, dfn) {
4584
+ text-decoration: underline dotted;
4585
+ }
4586
+ }
4587
+ }
4588
+
4124
4589
  /* ManifestToasts */
4125
4590
 
4126
4591
  @layer base, components, utilities;
@@ -4809,123 +5274,6 @@
4809
5274
  }
4810
5275
  }
4811
5276
 
4812
- /* Drag-handle geometry — single source for x-resize and x-edit handles.
4813
- The build appends this to standalone copies and adds it once to the bundle.
4814
- Hosts set --h (handle thickness) and keep their own chrome: hairline colour,
4815
- transition, and corner cursors (resize uses nw/ne/sw/se, edit nwse/nesw). */
4816
-
4817
- @layer base, components, utilities;
4818
-
4819
- @layer utilities {
4820
-
4821
- /* Horizontal edges */
4822
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top, .resize-handle-bottom, .edit-handle-top, .edit-handle-bottom) {
4823
- left: 0;
4824
- width: 100%;
4825
- height: var(--h);
4826
- cursor: ns-resize;
4827
-
4828
- &::before {
4829
- width: 100%
4830
- }
4831
- }
4832
-
4833
- /* Vertical edges */
4834
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-left, .resize-handle-right, .resize-handle-start, .resize-handle-end, .edit-handle-left, .edit-handle-right, .edit-handle-start, .edit-handle-end) {
4835
- top: 0;
4836
- width: var(--h);
4837
- height: 100%;
4838
- cursor: ew-resize;
4839
-
4840
- &::before {
4841
- height: 100%
4842
- }
4843
- }
4844
-
4845
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top, .edit-handle-top) {
4846
- top: calc(var(--h) * -0.5)
4847
- }
4848
-
4849
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-bottom, .edit-handle-bottom) {
4850
- bottom: calc(var(--h) * -0.5)
4851
- }
4852
-
4853
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-left, .edit-handle-left) {
4854
- left: calc(var(--h) * -0.5)
4855
- }
4856
-
4857
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-right, .edit-handle-right) {
4858
- right: calc(var(--h) * -0.5)
4859
- }
4860
-
4861
- /* Logical edges — inset-inline-* auto-flips for RTL */
4862
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-start, .edit-handle-start) {
4863
- inset-inline-start: calc(var(--h) * -0.5)
4864
- }
4865
-
4866
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-end, .edit-handle-end) {
4867
- inset-inline-end: calc(var(--h) * -0.5)
4868
- }
4869
-
4870
- /* Corners — square and inset; cursors stay with each host */
4871
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top-left, .edit-handle-top-left) {
4872
- width: var(--h);
4873
- height: var(--h);
4874
- top: calc(var(--h) * -0.5);
4875
- left: calc(var(--h) * -0.5)
4876
- }
4877
-
4878
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top-right, .edit-handle-top-right) {
4879
- width: var(--h);
4880
- height: var(--h);
4881
- top: calc(var(--h) * -0.5);
4882
- right: calc(var(--h) * -0.5)
4883
- }
4884
-
4885
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-bottom-left, .edit-handle-bottom-left) {
4886
- width: var(--h);
4887
- height: var(--h);
4888
- bottom: calc(var(--h) * -0.5);
4889
- left: calc(var(--h) * -0.5)
4890
- }
4891
-
4892
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-bottom-right, .edit-handle-bottom-right) {
4893
- width: var(--h);
4894
- height: var(--h);
4895
- bottom: calc(var(--h) * -0.5);
4896
- right: calc(var(--h) * -0.5)
4897
- }
4898
-
4899
- /* Logical corners — inset-inline-* auto-flips for RTL */
4900
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top-start, .edit-handle-top-start) {
4901
- width: var(--h);
4902
- height: var(--h);
4903
- top: calc(var(--h) * -0.5);
4904
- inset-inline-start: calc(var(--h) * -0.5)
4905
- }
4906
-
4907
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-top-end, .edit-handle-top-end) {
4908
- width: var(--h);
4909
- height: var(--h);
4910
- top: calc(var(--h) * -0.5);
4911
- inset-inline-end: calc(var(--h) * -0.5)
4912
- }
4913
-
4914
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-bottom-start, .edit-handle-bottom-start) {
4915
- width: var(--h);
4916
- height: var(--h);
4917
- bottom: calc(var(--h) * -0.5);
4918
- inset-inline-start: calc(var(--h) * -0.5)
4919
- }
4920
-
4921
- :where([x-resize], [data-edit-sizable]) :is(.resize-handle-bottom-end, .edit-handle-bottom-end) {
4922
- width: var(--h);
4923
- height: var(--h);
4924
- bottom: calc(var(--h) * -0.5);
4925
- inset-inline-end: calc(var(--h) * -0.5)
4926
- }
4927
- }
4928
-
4929
5277
  :root,
4930
5278
  ::selection {
4931
5279