@morphika/andami 0.5.2 → 0.5.4
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/README.md +27 -2
- package/app/admin/layout.tsx +26 -14
- package/app/admin/pages/[slug]/page.tsx +39 -22
- package/app/admin/pages/page.tsx +13 -8
- package/app/admin/projects/page.tsx +17 -8
- package/app/api/admin/assets/register/route.ts +51 -14
- package/app/api/admin/assets/registry/route.ts +4 -1
- package/app/api/admin/assets/relink/confirm/route.ts +4 -1
- package/app/api/admin/assets/relink/route.ts +4 -1
- package/app/api/admin/assets/scan/route.ts +4 -1
- package/app/api/admin/backups/restore-data/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +4 -1
- package/app/api/admin/r2/delete/route.ts +4 -1
- package/app/api/admin/r2/rename/route.ts +4 -1
- package/app/api/admin/r2/upload-url/route.ts +4 -1
- package/app/api/admin/revalidate/route.ts +4 -1
- package/app/api/admin/storage/switch/route.ts +4 -1
- package/app/api/custom-sections/[id]/route.ts +5 -6
- package/components/admin/PublishToggle.tsx +2 -2
- package/components/admin/nav-builder/NavGridItem.tsx +4 -2
- package/components/admin/nav-builder/NavSettingsFields.tsx +10 -6
- package/components/admin/styles/ColorsEditor.tsx +7 -6
- package/components/admin/styles/FontsEditor.tsx +3 -1
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BubbleIcons.tsx +14 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/SectionEditorBar.tsx +4 -2
- package/components/builder/SectionTypePicker.tsx +4 -2
- package/components/builder/SectionV2Column.tsx +13 -1
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +2 -2
- package/components/builder/SortableRow.tsx +6 -9
- package/components/builder/VirtualAssetGrid.tsx +8 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +8 -4
- package/components/builder/color-picker/EyedropperButton.tsx +7 -6
- package/components/builder/color-picker/SwatchBar.tsx +11 -6
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -2
- package/components/builder/editors/MarqueeBlockEditor.tsx +3 -2
- package/components/builder/editors/ProjectGridEditor.tsx +12 -7
- package/components/builder/editors/SpacerBlockEditor.tsx +25 -23
- package/components/builder/editors/TextBlockEditor.tsx +19 -14
- package/components/builder/editors/shared.tsx +4 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +3 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -1
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/BlockLayoutTab.tsx +4 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- package/components/builder/settings-panel/CoverSectionSettings.tsx +4 -2
- package/components/builder/settings-panel/SectionV2Settings.tsx +13 -8
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/ui/NavContentLightbox.tsx +41 -4
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-blocks.ts +15 -5
- package/lib/builder/store-cover.ts +16 -6
- package/lib/builder/store-sections.ts +151 -51
- package/lib/builder/types-slices.ts +14 -0
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +14 -0
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
|
@@ -4,6 +4,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
|
|
|
4
4
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
5
|
import { getCsrfToken } from "../../lib/csrf-client";
|
|
6
6
|
import { ADMIN_ACCENT } from "../../lib/builder/constants";
|
|
7
|
+
import { BubbleTooltip } from "./BubbleIcons";
|
|
7
8
|
|
|
8
9
|
// ============================================
|
|
9
10
|
// SectionEditorBar — Top bar for custom section editor mode
|
|
@@ -134,10 +135,11 @@ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarPro
|
|
|
134
135
|
<span className="text-[#444]">/</span>
|
|
135
136
|
<button
|
|
136
137
|
onClick={handleCancel}
|
|
137
|
-
className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
|
|
138
|
-
|
|
138
|
+
className="group/bb relative text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
|
|
139
|
+
aria-label={`Back to ${pageTitle || "page"}`}
|
|
139
140
|
>
|
|
140
141
|
{pageTitle || "Page"}
|
|
142
|
+
<BubbleTooltip>{`Back to ${pageTitle || "page"}`}</BubbleTooltip>
|
|
141
143
|
</button>
|
|
142
144
|
<span className="text-[#444]">/</span>
|
|
143
145
|
<span className="text-[#999] font-medium">
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
|
|
4
4
|
import { SECTION_TYPE_REGISTRY, type SectionBlockType } from "../../lib/builder/types";
|
|
5
5
|
import type { CustomSectionListItem } from "../../lib/sanity/types";
|
|
6
6
|
import { SECTION_CARD_ICONS } from "./SectionCardIcons";
|
|
7
|
+
import { BubbleTooltip } from "./BubbleIcons";
|
|
7
8
|
|
|
8
9
|
// ── V2 layout presets (use cascade preset names) ──
|
|
9
10
|
type V2Preset = "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3";
|
|
@@ -212,8 +213,8 @@ export default function SectionTypePicker({
|
|
|
212
213
|
}
|
|
213
214
|
onClose();
|
|
214
215
|
}}
|
|
215
|
-
className="rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#3580f9] hover:bg-[#3580f9]/5 transition-colors group shadow-sm"
|
|
216
|
-
|
|
216
|
+
className="group/bb relative rounded-xl border border-neutral-200 bg-white p-3 hover:border-[#3580f9] hover:bg-[#3580f9]/5 transition-colors group shadow-sm"
|
|
217
|
+
aria-label={label}
|
|
217
218
|
>
|
|
218
219
|
<div className="flex gap-1 h-6">
|
|
219
220
|
{widths.map((w, i) => (
|
|
@@ -227,6 +228,7 @@ export default function SectionTypePicker({
|
|
|
227
228
|
<p className="text-xs text-neutral-500 mt-1.5 group-hover:text-neutral-700">
|
|
228
229
|
{label}
|
|
229
230
|
</p>
|
|
231
|
+
<BubbleTooltip>{label}</BubbleTooltip>
|
|
230
232
|
</button>
|
|
231
233
|
))}
|
|
232
234
|
</div>
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
10
10
|
import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
|
|
11
11
|
import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getColumnVerticalAlign,
|
|
14
|
+
getBackgroundStyles,
|
|
15
|
+
getBorderStyles,
|
|
16
|
+
} from "../../lib/builder/layout-styles";
|
|
13
17
|
import { isSectionBlockType } from "../../lib/builder/types";
|
|
14
18
|
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
15
19
|
import { BubbleTooltip, CloseIcon, DragDropIcon } from "./BubbleIcons";
|
|
@@ -230,6 +234,12 @@ export default function SectionV2Column({
|
|
|
230
234
|
// Column-level vertical alignment from blocks' align_v settings
|
|
231
235
|
const colJustify = getColumnVerticalAlign(column.blocks || []);
|
|
232
236
|
|
|
237
|
+
// Column-level background + border (desktop-only — no responsive overrides).
|
|
238
|
+
const columnLayoutStyles: React.CSSProperties = {
|
|
239
|
+
...getBackgroundStyles(column),
|
|
240
|
+
...getBorderStyles(column),
|
|
241
|
+
};
|
|
242
|
+
|
|
233
243
|
// ---- Preview mode ----
|
|
234
244
|
if (previewMode) {
|
|
235
245
|
return (
|
|
@@ -244,6 +254,7 @@ export default function SectionV2Column({
|
|
|
244
254
|
...(colJustify ? { justifyContent: colJustify } : {}),
|
|
245
255
|
height: "100%",
|
|
246
256
|
minHeight: 0,
|
|
257
|
+
...columnLayoutStyles,
|
|
247
258
|
}}
|
|
248
259
|
>
|
|
249
260
|
<SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
|
|
@@ -280,6 +291,7 @@ export default function SectionV2Column({
|
|
|
280
291
|
transition: isDraggedColumn
|
|
281
292
|
? "none"
|
|
282
293
|
: "opacity 150ms, box-shadow 150ms, transform 150ms ease-out",
|
|
294
|
+
...columnLayoutStyles,
|
|
283
295
|
}}
|
|
284
296
|
ref={setBlockDropRef}
|
|
285
297
|
onClick={handleClick}
|
|
@@ -25,6 +25,7 @@ import { useBuilderStore } from "../../lib/builder/store";
|
|
|
25
25
|
import { useSettingsPanelSelection } from "./settings-panel/useSettingsPanelSelection";
|
|
26
26
|
import { AnimationTab } from "./settings-panel/AnimationTab";
|
|
27
27
|
import { ColumnV2AnimationTab } from "./settings-panel/ColumnV2AnimationTab";
|
|
28
|
+
import { ColumnV2LayoutTab } from "./settings-panel/ColumnV2LayoutTab";
|
|
28
29
|
import { CustomSectionSettings } from "./settings-panel/CustomSectionSettings";
|
|
29
30
|
import {
|
|
30
31
|
BlockLayoutTab,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
CoverSectionSettings,
|
|
41
42
|
} from "./settings-panel";
|
|
42
43
|
import CoverSectionLayoutTab from "./settings-panel/CoverSectionLayoutTab";
|
|
44
|
+
import { BubbleTooltip } from "./BubbleIcons";
|
|
43
45
|
|
|
44
46
|
type SettingsTab = "settings" | "layout" | "seo" | "animation";
|
|
45
47
|
|
|
@@ -75,11 +77,11 @@ export default function SettingsPanel() {
|
|
|
75
77
|
}
|
|
76
78
|
}, [selectionKey]);
|
|
77
79
|
|
|
78
|
-
// Columns have Settings + Animation — fall back if
|
|
80
|
+
// Columns have Settings + Layout + Animation — fall back if SEO tab was active
|
|
79
81
|
// Parallax group header has only Settings — fall back if Layout/SEO or Animation tab was active
|
|
80
82
|
// Page level has Settings + SEO + Animation — fall back if Layout tab was active
|
|
81
83
|
useEffect(() => {
|
|
82
|
-
if (isColumnOnly &&
|
|
84
|
+
if (isColumnOnly && activeTab === "seo") {
|
|
83
85
|
setActiveTab("settings");
|
|
84
86
|
}
|
|
85
87
|
if (isParallaxGroupOnly && (activeTab === "layout" || activeTab === "seo" || activeTab === "animation")) {
|
|
@@ -134,13 +136,14 @@ export default function SettingsPanel() {
|
|
|
134
136
|
return (
|
|
135
137
|
<button
|
|
136
138
|
onClick={onDelete}
|
|
137
|
-
className="p-1.5 rounded-md hover:bg-red-500/20 transition-colors
|
|
138
|
-
|
|
139
|
+
className="group/bb relative p-1.5 rounded-md hover:bg-red-500/20 transition-colors"
|
|
140
|
+
aria-label={deleteTitle}
|
|
139
141
|
>
|
|
140
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-black/40 group-hover:text-[var(--admin-error)] transition-colors">
|
|
142
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-black/40 group-hover/bb:text-[var(--admin-error)] transition-colors">
|
|
141
143
|
<polyline points="3 6 5 6 21 6" />
|
|
142
144
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
143
145
|
</svg>
|
|
146
|
+
<BubbleTooltip>{deleteTitle}</BubbleTooltip>
|
|
144
147
|
</button>
|
|
145
148
|
);
|
|
146
149
|
})()}
|
|
@@ -204,11 +207,6 @@ export default function SettingsPanel() {
|
|
|
204
207
|
});
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
// Columns: remove Layout tab (keep Settings + Animation)
|
|
208
|
-
if (isColumnOnly) {
|
|
209
|
-
const layoutIdx = tabs.findIndex((t) => t.id === "layout");
|
|
210
|
-
if (layoutIdx >= 0) tabs.splice(layoutIdx, 1);
|
|
211
|
-
}
|
|
212
210
|
// Parallax group header: only Settings (no Layout, no Animation)
|
|
213
211
|
if (isParallaxGroupOnly) {
|
|
214
212
|
const layoutIdx = tabs.findIndex((t) => t.id === "layout");
|
|
@@ -268,9 +266,11 @@ export default function SettingsPanel() {
|
|
|
268
266
|
{selectedParallaxGroup && !selectedParallaxSlide && !selectedBlock ? (
|
|
269
267
|
<ParallaxGroupSettings group={selectedParallaxGroup} />
|
|
270
268
|
) : selectedParallaxSlide && selectedColumnV2 && !selectedBlock ? (
|
|
271
|
-
// Column inside a parallax slide — Settings
|
|
269
|
+
// Column inside a parallax slide — Settings / Layout / Animation
|
|
272
270
|
activeTab === "animation" ? (
|
|
273
271
|
<ColumnV2AnimationTab section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
|
|
272
|
+
) : activeTab === "layout" ? (
|
|
273
|
+
<ColumnV2LayoutTab section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
|
|
274
274
|
) : (
|
|
275
275
|
<ColumnV2Settings section={selectedParallaxSlide.virtualSection} column={selectedColumnV2} />
|
|
276
276
|
)
|
|
@@ -320,15 +320,19 @@ export default function SettingsPanel() {
|
|
|
320
320
|
<CoverSectionSettings section={selectedCoverSection} />
|
|
321
321
|
)
|
|
322
322
|
) :
|
|
323
|
-
/* ---- V2 Section / Column / Block routing ---- */
|
|
323
|
+
/* ---- V2 / Cover Section / Column / Block routing ---- */
|
|
324
324
|
/* BUG-V2-003 fix: When a block inside a V2 column is selected, show BlockSettings
|
|
325
|
-
instead of ColumnV2Settings. Block selection takes priority over column.
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
instead of ColumnV2Settings. Block selection takes priority over column.
|
|
326
|
+
Cover-column fix: use effectiveSectionV2 so columns inside a cover section
|
|
327
|
+
route to ColumnV2Settings instead of falling through to PageSettings. */
|
|
328
|
+
selectedColumnV2 && effectiveSectionV2 && !selectedBlock ? (
|
|
329
|
+
// V2 Column or Cover Section column selected (no block) — Settings / Layout / Animation
|
|
328
330
|
activeTab === "animation" ? (
|
|
329
|
-
<ColumnV2AnimationTab section={
|
|
331
|
+
<ColumnV2AnimationTab section={effectiveSectionV2} column={selectedColumnV2} />
|
|
332
|
+
) : activeTab === "layout" ? (
|
|
333
|
+
<ColumnV2LayoutTab section={effectiveSectionV2} column={selectedColumnV2} />
|
|
330
334
|
) : (
|
|
331
|
-
<ColumnV2Settings section={
|
|
335
|
+
<ColumnV2Settings section={effectiveSectionV2} column={selectedColumnV2} />
|
|
332
336
|
)
|
|
333
337
|
) : selectedSectionV2 && !selectedBlock ? (
|
|
334
338
|
// V2 Section selected — route by active tab
|
|
@@ -189,11 +189,11 @@ export default function SortableBlock({
|
|
|
189
189
|
<>
|
|
190
190
|
<div className="w-px self-stretch my-1" style={{ background: "#3580f9" }} />
|
|
191
191
|
<span
|
|
192
|
-
className="text-[10px] px-1 py-0.5"
|
|
192
|
+
className="group/bb relative text-[10px] px-1 py-0.5"
|
|
193
193
|
style={{ color: "#3580f9" }}
|
|
194
|
-
title={`Animation: ${block.enter_animation.preset}`}
|
|
195
194
|
>
|
|
196
195
|
✦
|
|
196
|
+
<BubbleTooltip>{`Animation: ${block.enter_animation.preset}`}</BubbleTooltip>
|
|
197
197
|
</span>
|
|
198
198
|
</>
|
|
199
199
|
)}
|
|
@@ -264,7 +264,7 @@ export default function SortableRow({
|
|
|
264
264
|
style={{
|
|
265
265
|
transform: `translateX(calc(-100% - 8px)) scale(${Math.min(2, 1 / canvasZoom)})`,
|
|
266
266
|
transformOrigin: "top right",
|
|
267
|
-
width: "
|
|
267
|
+
width: "105px",
|
|
268
268
|
}}
|
|
269
269
|
onClick={(e) => {
|
|
270
270
|
e.stopPropagation();
|
|
@@ -341,7 +341,6 @@ export default function SortableRow({
|
|
|
341
341
|
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
342
342
|
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
343
343
|
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
344
|
-
title={`Add ${addColumnLabel.toLowerCase()}`}
|
|
345
344
|
aria-label={`Add ${addColumnLabel.toLowerCase()}`}
|
|
346
345
|
>
|
|
347
346
|
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> {addColumnLabel}
|
|
@@ -356,7 +355,6 @@ export default function SortableRow({
|
|
|
356
355
|
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
357
356
|
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
358
357
|
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
359
|
-
title="Delete section"
|
|
360
358
|
aria-label="Delete section"
|
|
361
359
|
>
|
|
362
360
|
<CloseIcon size={12} /> Delete
|
|
@@ -416,8 +414,7 @@ export default function SortableRow({
|
|
|
416
414
|
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
417
415
|
onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
|
|
418
416
|
onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
419
|
-
|
|
420
|
-
aria-label="Add row"
|
|
417
|
+
aria-label={canAddRow ? "Add row" : "Cover supports up to 5 rows"}
|
|
421
418
|
>
|
|
422
419
|
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> Row
|
|
423
420
|
</button>
|
|
@@ -517,7 +514,6 @@ export default function SortableRow({
|
|
|
517
514
|
style={{ color: "rgba(117, 0, 213, 0.6)" }}
|
|
518
515
|
onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
|
|
519
516
|
onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
|
|
520
|
-
title="Add slide"
|
|
521
517
|
aria-label="Add slide"
|
|
522
518
|
>
|
|
523
519
|
<span style={{ color: "rgba(117, 0, 213, 0.4)" }}>+</span> Slide
|
|
@@ -531,10 +527,11 @@ export default function SortableRow({
|
|
|
531
527
|
{bgColor !== "transparent" && isSelected && (
|
|
532
528
|
<div className="absolute top-1 right-1 z-[5]" style={{ transform: `scale(${1 / canvasZoom})`, transformOrigin: "top right" }}>
|
|
533
529
|
<span
|
|
534
|
-
className="w-4 h-4 rounded-full border-2 border-white/50 block shadow-sm"
|
|
530
|
+
className="group/bb relative w-4 h-4 rounded-full border-2 border-white/50 block shadow-sm"
|
|
535
531
|
style={{ backgroundColor: bgColor }}
|
|
536
|
-
|
|
537
|
-
|
|
532
|
+
>
|
|
533
|
+
<BubbleTooltip>{`Background: ${bgColor}`}</BubbleTooltip>
|
|
534
|
+
</span>
|
|
538
535
|
</div>
|
|
539
536
|
)}
|
|
540
537
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
16
16
|
import type { RegisteredAsset } from "../../lib/sanity/types";
|
|
17
17
|
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
18
|
+
import { BubbleTooltip } from "./BubbleIcons";
|
|
18
19
|
|
|
19
20
|
// ============================================
|
|
20
21
|
// Types
|
|
@@ -345,10 +346,10 @@ function AssetGridItem({
|
|
|
345
346
|
{/* Thumbnail status badge — raster images only */}
|
|
346
347
|
{isImageType(asset.extension) && asset.extension !== "svg" && (
|
|
347
348
|
<div
|
|
348
|
-
className={`absolute bottom-1.5 right-1.5 w-4 h-4 rounded-full flex items-center justify-center backdrop-blur-sm ${
|
|
349
|
+
className={`group/bb absolute bottom-1.5 right-1.5 w-4 h-4 rounded-full flex items-center justify-center backdrop-blur-sm ${
|
|
349
350
|
asset.has_thumbnail ? "bg-green-500/80" : "bg-amber-500/80"
|
|
350
351
|
}`}
|
|
351
|
-
|
|
352
|
+
aria-label={
|
|
352
353
|
asset.has_thumbnail
|
|
353
354
|
? "Thumbnail available"
|
|
354
355
|
: "No thumbnail — loading full resolution"
|
|
@@ -382,6 +383,11 @@ function AssetGridItem({
|
|
|
382
383
|
<circle cx="12" cy="16" r="0.5" fill="white" />
|
|
383
384
|
</svg>
|
|
384
385
|
)}
|
|
386
|
+
<BubbleTooltip>
|
|
387
|
+
{asset.has_thumbnail
|
|
388
|
+
? "Thumbnail available"
|
|
389
|
+
: "No thumbnail — loading full resolution"}
|
|
390
|
+
</BubbleTooltip>
|
|
385
391
|
</div>
|
|
386
392
|
)}
|
|
387
393
|
</div>
|
|
@@ -12,6 +12,7 @@ import { useR2Operations } from "./useR2Operations";
|
|
|
12
12
|
import { useR2DragDrop } from "./useR2DragDrop";
|
|
13
13
|
import { R2ContextMenu, type ContextMenuState } from "./R2ContextMenu";
|
|
14
14
|
import { ADMIN_ACCENT, BUILDER_GREEN } from "../../../lib/builder/constants";
|
|
15
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
15
16
|
|
|
16
17
|
// ============================================
|
|
17
18
|
// R2 Browser — Composition shell
|
|
@@ -291,18 +292,19 @@ export function R2BrowserContent({
|
|
|
291
292
|
))}
|
|
292
293
|
</div>
|
|
293
294
|
<div className="flex items-center gap-2">
|
|
294
|
-
<button onClick={ops.openNewFolderInput} disabled={ops.actionLoading} className="inline-flex items-center gap-1.5 rounded-lg bg-neutral-100 px-3 py-1.5 text-[11px] text-neutral-700 font-medium uppercase tracking-wider hover:bg-neutral-200 transition-colors disabled:opacity-50"
|
|
295
|
+
<button onClick={ops.openNewFolderInput} disabled={ops.actionLoading} className="group/bb relative inline-flex items-center gap-1.5 rounded-lg bg-neutral-100 px-3 py-1.5 text-[11px] text-neutral-700 font-medium uppercase tracking-wider hover:bg-neutral-200 transition-colors disabled:opacity-50" aria-label="Create a new folder" type="button">
|
|
295
296
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
296
297
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
297
298
|
<line x1="12" y1="11" x2="12" y2="17" /><line x1="9" y1="14" x2="15" y2="14" />
|
|
298
299
|
</svg>
|
|
299
300
|
New Folder
|
|
301
|
+
<BubbleTooltip>Create a new folder</BubbleTooltip>
|
|
300
302
|
</button>
|
|
301
303
|
<button
|
|
302
304
|
onClick={() => ops.fileInputRef.current?.click()}
|
|
303
305
|
disabled={uploading.some((u) => u.status === "uploading" || u.status === "registering")}
|
|
304
|
-
className="inline-flex items-center gap-1.5 rounded-lg bg-[#3580f9] px-3 py-1.5 text-[11px] text-white font-medium uppercase tracking-wider hover:bg-[#3580f9]/90 transition-colors disabled:opacity-50"
|
|
305
|
-
|
|
306
|
+
className="group/bb relative inline-flex items-center gap-1.5 rounded-lg bg-[#3580f9] px-3 py-1.5 text-[11px] text-white font-medium uppercase tracking-wider hover:bg-[#3580f9]/90 transition-colors disabled:opacity-50"
|
|
307
|
+
aria-label={`Upload files${currentFolder ? ` to ${currentFolder}` : ""}`}
|
|
306
308
|
type="button"
|
|
307
309
|
>
|
|
308
310
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
@@ -311,6 +313,7 @@ export function R2BrowserContent({
|
|
|
311
313
|
<line x1="12" y1="3" x2="12" y2="15" />
|
|
312
314
|
</svg>
|
|
313
315
|
Upload
|
|
316
|
+
<BubbleTooltip>{`Upload files${currentFolder ? ` to ${currentFolder}` : ""}`}</BubbleTooltip>
|
|
314
317
|
</button>
|
|
315
318
|
</div>
|
|
316
319
|
</div>
|
|
@@ -323,8 +326,9 @@ export function R2BrowserContent({
|
|
|
323
326
|
{u.status === "done" ? (
|
|
324
327
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={BUILDER_GREEN} strokeWidth="2.5"><polyline points="20 6 9 17 4 12" /></svg>
|
|
325
328
|
) : u.status === "error" ? (
|
|
326
|
-
<button onClick={() => onClearUploadError?.(u.id)}
|
|
329
|
+
<button onClick={() => onClearUploadError?.(u.id)} className="group/bb relative" aria-label="Dismiss">
|
|
327
330
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
|
331
|
+
<BubbleTooltip>Dismiss</BubbleTooltip>
|
|
328
332
|
</button>
|
|
329
333
|
) : (
|
|
330
334
|
<div className="w-3.5 h-3.5 border-2 border-[#3580f9] border-t-transparent rounded-full animate-spin" />
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useState, useCallback } from "react";
|
|
12
12
|
import type { EyedropperButtonProps } from "./types";
|
|
13
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
13
14
|
|
|
14
15
|
/** Check if the EyeDropper API is available */
|
|
15
16
|
function isEyeDropperSupported(): boolean {
|
|
@@ -39,17 +40,16 @@ export default function EyedropperButton({
|
|
|
39
40
|
}
|
|
40
41
|
}, [supported, picking, onColorPicked]);
|
|
41
42
|
|
|
43
|
+
const label = supported
|
|
44
|
+
? "Pick a color from screen"
|
|
45
|
+
: "Eyedropper not supported in this browser";
|
|
42
46
|
return (
|
|
43
47
|
<button
|
|
44
48
|
type="button"
|
|
45
49
|
onClick={handleClick}
|
|
46
50
|
disabled={!supported}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? "Pick a color from screen"
|
|
50
|
-
: "Eyedropper not supported in this browser"
|
|
51
|
-
}
|
|
52
|
-
className={`w-10 h-10 rounded-[10px] border shrink-0 flex items-center justify-center transition-colors ${
|
|
51
|
+
aria-label={label}
|
|
52
|
+
className={`group/bb relative w-10 h-10 rounded-[10px] border shrink-0 flex items-center justify-center transition-colors ${
|
|
53
53
|
supported
|
|
54
54
|
? "border-neutral-200 bg-neutral-50 text-neutral-500 cursor-pointer hover:border-neutral-300 hover:text-neutral-700"
|
|
55
55
|
: "border-neutral-100 bg-neutral-50 text-neutral-300 cursor-not-allowed"
|
|
@@ -69,6 +69,7 @@ export default function EyedropperButton({
|
|
|
69
69
|
<path d="M3 21v-3l9-9" />
|
|
70
70
|
<path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3L15 6" />
|
|
71
71
|
</svg>
|
|
72
|
+
<BubbleTooltip>{label}</BubbleTooltip>
|
|
72
73
|
</button>
|
|
73
74
|
);
|
|
74
75
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback } from "react";
|
|
12
12
|
import type { SwatchBarProps } from "./types";
|
|
13
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
13
14
|
|
|
14
15
|
// Common neutral colors always available
|
|
15
16
|
const COMMON_COLORS = [
|
|
@@ -51,14 +52,16 @@ export default function SwatchBar({
|
|
|
51
52
|
key={s._key || `swatch-${i}`}
|
|
52
53
|
type="button"
|
|
53
54
|
onClick={() => handleSwatchClick(s.hex)}
|
|
54
|
-
|
|
55
|
-
className={`w-8 h-8 rounded-lg cursor-pointer transition-all ${
|
|
55
|
+
aria-label={`${s.name}: ${s.hex}`}
|
|
56
|
+
className={`group/bb relative w-8 h-8 rounded-lg cursor-pointer transition-all ${
|
|
56
57
|
value.toLowerCase() === s.hex.toLowerCase()
|
|
57
58
|
? "ring-2 ring-[#3580f9] ring-offset-1 ring-offset-white"
|
|
58
59
|
: "border border-neutral-200 hover:border-neutral-400 hover:scale-110"
|
|
59
60
|
}`}
|
|
60
61
|
style={{ background: s.hex }}
|
|
61
|
-
|
|
62
|
+
>
|
|
63
|
+
<BubbleTooltip>{`${s.name}: ${s.hex}`}</BubbleTooltip>
|
|
64
|
+
</button>
|
|
62
65
|
))}
|
|
63
66
|
</div>
|
|
64
67
|
</div>
|
|
@@ -77,14 +80,16 @@ export default function SwatchBar({
|
|
|
77
80
|
key={c}
|
|
78
81
|
type="button"
|
|
79
82
|
onClick={() => handleSwatchClick(c)}
|
|
80
|
-
|
|
81
|
-
className={`w-6 h-6 rounded-md cursor-pointer transition-all ${
|
|
83
|
+
aria-label={c.toUpperCase()}
|
|
84
|
+
className={`group/bb relative w-6 h-6 rounded-md cursor-pointer transition-all ${
|
|
82
85
|
value.toLowerCase() === c
|
|
83
86
|
? "ring-2 ring-[#3580f9] ring-offset-1 ring-offset-white"
|
|
84
87
|
: "border border-neutral-200 hover:border-neutral-400 hover:scale-110"
|
|
85
88
|
}`}
|
|
86
89
|
style={{ background: c }}
|
|
87
|
-
|
|
90
|
+
>
|
|
91
|
+
<BubbleTooltip>{c.toUpperCase()}</BubbleTooltip>
|
|
92
|
+
</button>
|
|
88
93
|
))}
|
|
89
94
|
</div>
|
|
90
95
|
</div>
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { getPresetsForCategory } from "../../../lib/builder/gradient-presets";
|
|
46
46
|
import type { GradientPresetInfo } from "../../../lib/builder/gradient-presets";
|
|
47
47
|
import type { ColorSwatch } from "../../../lib/sanity/types";
|
|
48
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
48
49
|
|
|
49
50
|
type GradientTab = "solid" | "linear" | "radial" | "mesh";
|
|
50
51
|
|
|
@@ -554,11 +555,13 @@ export default function UnifiedColorPicker({
|
|
|
554
555
|
<button
|
|
555
556
|
key={preset.id}
|
|
556
557
|
type="button"
|
|
557
|
-
|
|
558
|
+
aria-label={preset.label}
|
|
558
559
|
onClick={() => handlePresetSelect(preset)}
|
|
559
|
-
className="w-8 h-8 rounded-lg border border-neutral-200 hover:border-neutral-400 transition-colors cursor-pointer shrink-0"
|
|
560
|
+
className="group/bb relative w-8 h-8 rounded-lg border border-neutral-200 hover:border-neutral-400 transition-colors cursor-pointer shrink-0"
|
|
560
561
|
style={{ backgroundImage: colorToCSS(preset.template) }}
|
|
561
|
-
|
|
562
|
+
>
|
|
563
|
+
<BubbleTooltip>{preset.label}</BubbleTooltip>
|
|
564
|
+
</button>
|
|
562
565
|
))}
|
|
563
566
|
</div>
|
|
564
567
|
)}
|
|
@@ -678,11 +681,13 @@ export default function UnifiedColorPicker({
|
|
|
678
681
|
<button
|
|
679
682
|
key={preset.id}
|
|
680
683
|
type="button"
|
|
681
|
-
|
|
684
|
+
aria-label={preset.label}
|
|
682
685
|
onClick={() => handlePresetSelect(preset)}
|
|
683
|
-
className="w-8 h-8 rounded-lg border border-neutral-200 hover:border-neutral-400 transition-colors cursor-pointer shrink-0"
|
|
686
|
+
className="group/bb relative w-8 h-8 rounded-lg border border-neutral-200 hover:border-neutral-400 transition-colors cursor-pointer shrink-0"
|
|
684
687
|
style={{ backgroundImage: colorToCSS(preset.template) }}
|
|
685
|
-
|
|
688
|
+
>
|
|
689
|
+
<BubbleTooltip>{preset.label}</BubbleTooltip>
|
|
690
|
+
</button>
|
|
686
691
|
))}
|
|
687
692
|
</div>
|
|
688
693
|
)}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useActiveViewport,
|
|
20
20
|
SELECT_CLASS,
|
|
21
21
|
} from "./shared";
|
|
22
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
22
23
|
|
|
23
24
|
interface Props {
|
|
24
25
|
block: ImageGridBlock;
|
|
@@ -74,13 +75,14 @@ function ImageThumb({
|
|
|
74
75
|
e.stopPropagation();
|
|
75
76
|
onRemove();
|
|
76
77
|
}}
|
|
77
|
-
className="w-7 h-7 rounded-full bg-white/90 hover:bg-[var(--admin-error)] hover:text-white flex items-center justify-center transition-colors text-neutral-600"
|
|
78
|
-
|
|
78
|
+
className="group/bb relative w-7 h-7 rounded-full bg-white/90 hover:bg-[var(--admin-error)] hover:text-white flex items-center justify-center transition-colors text-neutral-600"
|
|
79
|
+
aria-label="Remove image"
|
|
79
80
|
>
|
|
80
81
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
81
82
|
<polyline points="3 6 5 6 21 6" />
|
|
82
83
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
83
84
|
</svg>
|
|
85
|
+
<BubbleTooltip>Remove image</BubbleTooltip>
|
|
84
86
|
</button>
|
|
85
87
|
</div>
|
|
86
88
|
)}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
TypographyIcon,
|
|
38
38
|
LayoutIcon,
|
|
39
39
|
} from "./section-icons";
|
|
40
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
40
41
|
|
|
41
42
|
// ============================================
|
|
42
43
|
// Constants
|
|
@@ -271,11 +272,11 @@ function ItemRow({
|
|
|
271
272
|
>
|
|
272
273
|
<div className="flex items-center gap-2 mb-1.5">
|
|
273
274
|
<span
|
|
274
|
-
className="text-neutral-300 cursor-grab active:cursor-grabbing select-none"
|
|
275
|
-
title="Drag to reorder"
|
|
275
|
+
className="group/bb relative text-neutral-300 cursor-grab active:cursor-grabbing select-none"
|
|
276
276
|
aria-label="Drag handle"
|
|
277
277
|
>
|
|
278
278
|
⋮⋮
|
|
279
|
+
<BubbleTooltip>Drag to reorder</BubbleTooltip>
|
|
279
280
|
</span>
|
|
280
281
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-neutral-400">
|
|
281
282
|
{ITEM_TYPE_LABEL[item._type]} · #{index + 1}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ViewportBadge,
|
|
25
25
|
StyledCheckbox,
|
|
26
26
|
} from "./shared";
|
|
27
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
27
28
|
|
|
28
29
|
// ============================================
|
|
29
30
|
// Constants
|
|
@@ -516,7 +517,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
516
517
|
onClick={() => selectProjectCard(isCardSelected ? null : item._key)}
|
|
517
518
|
>
|
|
518
519
|
{/* Drag grip */}
|
|
519
|
-
<span className="text-neutral-300 shrink-0 cursor-grab"
|
|
520
|
+
<span className="group/bb relative text-neutral-300 shrink-0 cursor-grab" aria-label="Reorder">
|
|
520
521
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
|
521
522
|
<circle cx="3" cy="2" r="1" />
|
|
522
523
|
<circle cx="7" cy="2" r="1" />
|
|
@@ -525,6 +526,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
525
526
|
<circle cx="3" cy="8" r="1" />
|
|
526
527
|
<circle cx="7" cy="8" r="1" />
|
|
527
528
|
</svg>
|
|
529
|
+
<BubbleTooltip>Reorder</BubbleTooltip>
|
|
528
530
|
</span>
|
|
529
531
|
|
|
530
532
|
{/* Project name */}
|
|
@@ -543,34 +545,37 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
543
545
|
<button
|
|
544
546
|
onClick={(e) => { e.stopPropagation(); moveProject(i, -1); }}
|
|
545
547
|
disabled={i === 0}
|
|
546
|
-
className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
|
|
547
|
-
|
|
548
|
+
className="group/bb relative p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
|
|
549
|
+
aria-label="Move up"
|
|
548
550
|
>
|
|
549
551
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
550
552
|
<polyline points="18 15 12 9 6 15" />
|
|
551
553
|
</svg>
|
|
554
|
+
<BubbleTooltip>Move up</BubbleTooltip>
|
|
552
555
|
</button>
|
|
553
556
|
<button
|
|
554
557
|
onClick={(e) => { e.stopPropagation(); moveProject(i, 1); }}
|
|
555
558
|
disabled={i === (block.projects || []).length - 1}
|
|
556
|
-
className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
|
|
557
|
-
|
|
559
|
+
className="group/bb relative p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
|
|
560
|
+
aria-label="Move down"
|
|
558
561
|
>
|
|
559
562
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
560
563
|
<polyline points="6 9 12 15 18 9" />
|
|
561
564
|
</svg>
|
|
565
|
+
<BubbleTooltip>Move down</BubbleTooltip>
|
|
562
566
|
</button>
|
|
563
567
|
|
|
564
568
|
{/* Remove */}
|
|
565
569
|
<button
|
|
566
570
|
onClick={(e) => { e.stopPropagation(); removeProject(item._key); }}
|
|
567
|
-
className="p-0.5 text-neutral-400 hover:text-red-500 transition-colors"
|
|
568
|
-
|
|
571
|
+
className="group/bb relative p-0.5 text-neutral-400 hover:text-red-500 transition-colors"
|
|
572
|
+
aria-label="Remove"
|
|
569
573
|
>
|
|
570
574
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
571
575
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
572
576
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
573
577
|
</svg>
|
|
578
|
+
<BubbleTooltip>Remove</BubbleTooltip>
|
|
574
579
|
</button>
|
|
575
580
|
</div>
|
|
576
581
|
|