@morphika/andami 0.5.6 → 0.5.8
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/app/admin/layout.tsx +142 -98
- package/app/admin/styles/page.tsx +17 -1
- package/app/api/admin/styles/route.ts +18 -1
- package/app/api/styles/route.ts +5 -0
- package/components/admin/styles/CursorEditor.tsx +108 -0
- package/components/admin/styles/index.ts +1 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +6 -2
- package/components/blocks/SectionV2Renderer.tsx +14 -2
- package/components/ui/CustomCursor.tsx +138 -118
- package/lib/sanity/queries.ts +4 -1
- package/lib/sanity/types.ts +9 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/siteStyles.ts +27 -0
- package/styles/base.css +26 -0
package/app/admin/layout.tsx
CHANGED
|
@@ -50,149 +50,189 @@ function Tooltip({ children }: { children: React.ReactNode }) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// ============================================
|
|
53
|
-
// Andami Mark —
|
|
53
|
+
// Andami Mark — three-line scaffolding mark
|
|
54
54
|
// ============================================
|
|
55
55
|
|
|
56
56
|
function AndamiMark({ size = 30 }: { size?: number }) {
|
|
57
|
-
// viewBox
|
|
58
|
-
//
|
|
59
|
-
//
|
|
57
|
+
// viewBox tightened from the source 0 0 128 128 to the content bbox
|
|
58
|
+
// (incl. stroke caps), centered on the visual centroid (63.5, 60.5),
|
|
59
|
+
// so the mark fills the white tile without empty padding.
|
|
60
60
|
return (
|
|
61
61
|
<svg
|
|
62
62
|
width={size}
|
|
63
63
|
height={size}
|
|
64
|
-
viewBox="
|
|
64
|
+
viewBox="7.5 4.5 112 112"
|
|
65
65
|
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
fill="none"
|
|
67
|
+
stroke="#1E2025"
|
|
68
|
+
strokeWidth={10}
|
|
69
|
+
strokeLinecap="round"
|
|
70
|
+
strokeLinejoin="round"
|
|
66
71
|
aria-hidden
|
|
67
72
|
>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/>
|
|
72
|
-
<path
|
|
73
|
-
fill="#1E2025"
|
|
74
|
-
d="M227.2,185.8h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6c3.1,0,5.7-2.6,5.7-5.7v-39.6 C232.9,188.4,230.3,185.8,227.2,185.8z"
|
|
75
|
-
/>
|
|
76
|
-
<path
|
|
77
|
-
fill="#1E2025"
|
|
78
|
-
d="M311.4,231.1v-39.6c0-3.1-2.6-5.7-5.7-5.7h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6 C308.8,236.8,311.4,234.3,311.4,231.1z"
|
|
79
|
-
/>
|
|
80
|
-
<path
|
|
81
|
-
fill="#1E2025"
|
|
82
|
-
d="M332.7,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C338.7,261.1,336,258.4,332.7,258.4z"
|
|
83
|
-
/>
|
|
84
|
-
<path
|
|
85
|
-
fill="#1E2025"
|
|
86
|
-
d="M208.1,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C214.2,261.1,211.5,258.4,208.1,258.4z"
|
|
87
|
-
/>
|
|
73
|
+
<line x1="16.5" y1="108.5" x2="63.5" y2="12.5" />
|
|
74
|
+
<line x1="110.5" y1="108.5" x2="63.5" y2="12.5" />
|
|
75
|
+
<line x1="84.5" y1="108.5" x2="37.5" y2="12.5" />
|
|
88
76
|
</svg>
|
|
89
77
|
);
|
|
90
78
|
}
|
|
91
79
|
|
|
92
80
|
// ============================================
|
|
93
|
-
// Icon Component —
|
|
81
|
+
// Icon Component — inlined from docs/Design/svg/*.svg
|
|
94
82
|
// ============================================
|
|
83
|
+
// Source SVGs live at docs/Design/svg/ as outline/filled pairs so the design
|
|
84
|
+
// files are editable in Illustrator/Figma. Each section icon has two states:
|
|
85
|
+
// outline (default) + filled (when the section is active). Footer links that
|
|
86
|
+
// don't represent a section (View Site, Log out) only have a single variant.
|
|
87
|
+
// The runtime renders inline JSX (the framework has no public/ folder), so
|
|
88
|
+
// edits to the .svg need to be hand-synced into the switch below.
|
|
95
89
|
|
|
96
|
-
function NavIcon({ icon }: { icon: string }) {
|
|
97
|
-
const size =
|
|
90
|
+
function NavIcon({ icon, active = false }: { icon: string; active?: boolean }) {
|
|
91
|
+
const size = 28;
|
|
92
|
+
const svgProps = {
|
|
93
|
+
width: size,
|
|
94
|
+
height: size,
|
|
95
|
+
viewBox: "0 0 46 46",
|
|
96
|
+
fill: "currentColor" as const,
|
|
97
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
98
|
+
"aria-hidden": true,
|
|
99
|
+
};
|
|
98
100
|
switch (icon) {
|
|
99
101
|
case "file":
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<path d="
|
|
109
|
-
<path d="M14 13l1 2l-1 2" />
|
|
102
|
+
// Pages — "new page" / "new page_outline".
|
|
103
|
+
return active ? (
|
|
104
|
+
<svg {...svgProps}>
|
|
105
|
+
<path d="M23,6.6l0.2,0c0.8,0.1,1.3,0.7,1.4,1.4l0,0.2v6.6l0,0.2c0.1,1.6,1.4,2.9,3,3l0.3,0h6.6l0.2,0c0.8,0.1,1.3,0.7,1.4,1.4l0,0.2v14.8c0,2.6-2,4.8-4.6,4.9l-0.3,0H14.8c-2.6,0-4.8-2-4.9-4.6l0-0.3v-23c0-2.6,2-4.8,4.6-4.9l0.3,0C14.8,6.6,23,6.6,23,6.6z" />
|
|
106
|
+
<path d="M33.7,15h-5.2c-0.4,0-0.8-0.3-0.8-0.8l0-5.2c0-0.7,0.8-1,1.3-0.5l5.2,5.2C34.7,14.1,34.4,15,33.7,15z" />
|
|
107
|
+
</svg>
|
|
108
|
+
) : (
|
|
109
|
+
<svg {...svgProps}>
|
|
110
|
+
<path d="M36,15.7c-0.1-0.2-0.2-0.3-0.3-0.4l-8.5-8.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.5-0.1H14.5c-2.6,0-4.6,2.1-4.6,4.6v23.8c0,2.6,2.1,4.6,4.6,4.6h17c2.6,0,4.6-2.1,4.6-4.6V16.2C36.1,16,36.1,15.9,36,15.7z M27.6,10.7l4.3,4.3h-3.8c-0.3,0-0.5-0.2-0.5-0.5V10.7z M31.5,37h-17c-1.2,0-2.2-1-2.2-2.2V11.1c0-1.2,1-2.2,2.2-2.2h10.6v5.6c0,1.6,1.3,2.9,2.9,2.9h5.6v17.4C33.6,36.1,32.7,37,31.5,37z" />
|
|
110
111
|
</svg>
|
|
111
112
|
);
|
|
112
113
|
case "film":
|
|
113
|
-
//
|
|
114
|
-
return (
|
|
115
|
-
<svg
|
|
116
|
-
<path d="
|
|
117
|
-
<path d="
|
|
118
|
-
|
|
114
|
+
// Projects — "project" / "project_outline".
|
|
115
|
+
return active ? (
|
|
116
|
+
<svg {...svgProps}>
|
|
117
|
+
<path d="M33.7,15h-5.2c-0.4,0-0.8-0.3-0.8-0.8l0-5.2c0-0.7,0.8-1,1.3-0.5l5.2,5.2C34.7,14.1,34.4,15,33.7,15z" />
|
|
118
|
+
<path d="M36.1,19.5c-0.1-0.8-0.7-1.3-1.4-1.4l-0.2,0h-6.6l-0.3,0c-1.6-0.1-2.9-1.4-3-3l0-0.2V8.2l0-0.2c-0.1-0.8-0.7-1.3-1.4-1.4l-0.2,0h-8.2l-0.3,0c-2.6,0.2-4.6,2.3-4.6,4.9v23l0,0.3c0.2,2.6,2.3,4.6,4.9,4.6h16.4l0.3,0c2.6-0.2,4.6-2.3,4.6-4.9V19.7L36.1,19.5z M16.4,24.3l-0.2-0.2c-1.1-1.2-1-3.1,0.2-4.2c1.2-1.2,3.2-1.2,4.4,0l0.7,0.7L17.1,25L16.4,24.3z M29.5,32c0,0.6-0.4,1-1,1h-3c-0.3,0-0.5-0.1-0.7-0.3l-6.3-6.3l4.4-4.4l6.3,6.3c0.2,0.2,0.3,0.4,0.3,0.7V32z" />
|
|
119
|
+
</svg>
|
|
120
|
+
) : (
|
|
121
|
+
<svg {...svgProps}>
|
|
122
|
+
<path d="M36,15.7c-0.1-0.2-0.2-0.3-0.3-0.4l-8.5-8.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.5-0.1H14.5c-2.6,0-4.6,2.1-4.6,4.6v23.8c0,2.6,2.1,4.6,4.6,4.6h17c2.6,0,4.6-2.1,4.6-4.6V16.2C36.1,16,36.1,15.9,36,15.7z M27.6,10.7l4.3,4.3h-3.8c-0.3,0-0.5-0.2-0.5-0.5V10.7z M33.6,34.9c0,1.2-1,2.2-2.2,2.2h-17c-1.2,0-2.2-1-2.2-2.2V11.1c0-1.2,1-2.2,2.2-2.2h10.6v5.6c0,1.6,1.3,2.9,2.9,2.9h5.6V34.9z" />
|
|
123
|
+
<path d="M30.2,25.5L24.7,20l0,0l-1.6-1.6c-1.7-1.7-4.5-1.7-6.1,0c-1.6,1.6-1.7,4.2-0.2,5.9l0.1,0.1l0,0l7.2,7.2c0.4,0.4,1,0.7,1.6,0.7h3c1.2,0,2.2-1,2.2-2.2v-3C30.9,26.5,30.6,25.9,30.2,25.5z M21.2,20l-2.6,2.6c-0.5-0.7-0.5-1.8,0.2-2.4c0.3-0.3,0.8-0.5,1.3-0.5C20.5,19.7,20.9,19.8,21.2,20z M28.4,27.2v2.6h-2.6l-5.3-5.3l2.6-2.6L28.4,27.2z" />
|
|
119
124
|
</svg>
|
|
120
125
|
);
|
|
121
126
|
case "palette":
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<path
|
|
127
|
-
<path d="
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
// Customize — "customization" / "customization_outline".
|
|
128
|
+
return active ? (
|
|
129
|
+
<svg {...svgProps}>
|
|
130
|
+
<path d="M39.7,29.1c0-1.7-1.4-3-3.1-3h-1.8L21.3,39.5h15.4c1.7,0,3.1-1.4,3.1-3V29.1z" />
|
|
131
|
+
<path d="M19.9,9.5c0-1.7-1.4-3-3.1-3H9.4c-1.7,0-3.1,1.4-3.1,3v23.3c0,3.7,3,6.7,6.8,6.7c3.7,0,6.8-3,6.8-6.7V9.5z M13.1,35.9c-1.4,0-2.5-1.1-2.5-2.5c0-1.4,1.1-2.5,2.5-2.5c1.4,0,2.5,1.1,2.5,2.5C15.6,34.8,14.5,35.9,13.1,35.9z" />
|
|
132
|
+
<path d="M34.5,17.3l-5.7-5.6c-1.2-1.2-3.4-1.2-4.7,0L22.7,13v20.5l11.7-11.6c0.6-0.6,1-1.4,1-2.3C35.4,18.7,35.1,17.9,34.5,17.3z" />
|
|
133
|
+
</svg>
|
|
134
|
+
) : (
|
|
135
|
+
<svg {...svgProps}>
|
|
136
|
+
<path d="M6.5,11.1v20.4c0,4.4,3.6,8,8,8h20.4c2.5,0,4.6-2.1,4.6-4.6v-6.8c0-2.5-2.1-4.6-4.6-4.6h-1l1.3-1.3c1.8-1.8,1.8-4.7,0-6.5l-4.8-4.8c-0.9-0.9-2-1.4-3.3-1.4c-1.2,0-2.4,0.5-3.3,1.4l-1.3,1.3v-1.1c0-2.5-2.1-4.6-4.6-4.6h-6.8C8.6,6.5,6.5,8.6,6.5,11.1z M33.4,20.5L22.5,31.4V15.6l3-3c0.8-0.8,2.3-0.8,3.1,0l4.8,4.8C34.3,18.2,34.3,19.6,33.4,20.5z M34.9,37.1H20.3l11.2-11.2h3.5c1.2,0,2.2,1,2.2,2.2v6.8C37.1,36.1,36.1,37.1,34.9,37.1z M8.9,11.1c0-1.2,1-2.2,2.2-2.2h6.8c1.2,0,2.2,1,2.2,2.2v20.4c0,3.1-2.5,5.6-5.6,5.6c-3.1,0-5.6-2.5-5.6-5.6V11.1z" />
|
|
137
|
+
<path d="M14.5,30.3c0.7,0,1.2,0.6,1.2,1.2c0,0.7-0.5,1.2-1.2,1.2s-1.2-0.5-1.2-1.2v0C13.3,30.8,13.8,30.3,14.5,30.3z" />
|
|
131
138
|
</svg>
|
|
132
139
|
);
|
|
133
140
|
case "nav":
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
<
|
|
141
|
+
// Navigation — "navigation" / "navigation_outline".
|
|
142
|
+
return active ? (
|
|
143
|
+
<svg {...svgProps}>
|
|
144
|
+
<path d="M34,7.9H12c-1.5,0-2.7,1.2-2.7,2.7v0.8c0,1.5,1.2,2.7,2.7,2.7h22c1.5,0,2.7-1.2,2.7-2.7v-0.8C36.7,9.1,35.5,7.9,34,7.9z" />
|
|
145
|
+
<path d="M34,19.9H12c-1.5,0-2.7,1.2-2.7,2.7v0.8c0,1.5,1.2,2.7,2.7,2.7h22c1.5,0,2.7-1.2,2.7-2.7v-0.8C36.7,21.1,35.5,19.9,34,19.9z" />
|
|
146
|
+
<path d="M34,31.9H12c-1.5,0-2.7,1.2-2.7,2.7v0.8c0,1.5,1.2,2.7,2.7,2.7h22c1.5,0,2.7-1.2,2.7-2.7v-0.8C36.7,33.1,35.5,31.9,34,31.9z" />
|
|
139
147
|
</svg>
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
<path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6" />
|
|
146
|
-
<path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
|
|
148
|
+
) : (
|
|
149
|
+
<svg {...svgProps}>
|
|
150
|
+
<path d="M33,6.5H13c-2.1,0-3.9,1.7-3.9,3.9v0.4c0,2.1,1.7,3.9,3.9,3.9H33c2.1,0,3.9-1.7,3.9-3.9v-0.4C36.8,8.2,35.1,6.5,33,6.5z M34.4,10.8c0,0.8-0.6,1.4-1.4,1.4H13c-0.8,0-1.4-0.6-1.4-1.4v-0.4c0-0.8,0.6-1.4,1.4-1.4H33c0.8,0,1.4,0.6,1.4,1.4V10.8z" />
|
|
151
|
+
<path d="M33,18.4H13c-2.1,0-3.9,1.7-3.9,3.9v0.4c0,2.1,1.7,3.9,3.9,3.9H33c2.1,0,3.9-1.7,3.9-3.9v-0.4C36.8,20.2,35.1,18.4,33,18.4z M34.4,22.7c0,0.8-0.6,1.4-1.4,1.4H13c-0.8,0-1.4-0.6-1.4-1.4v-0.4c0-0.8,0.6-1.4,1.4-1.4H33c0.8,0,1.4,0.6,1.4,1.4V22.7z" />
|
|
152
|
+
<path d="M33,31.3H13c-2.1,0-3.9,1.7-3.9,3.9v0.4c0,2.1,1.7,3.9,3.9,3.9H33c2.1,0,3.9-1.7,3.9-3.9v-0.4C36.8,33.1,35.1,31.3,33,31.3z M34.4,35.6c0,0.8-0.6,1.4-1.4,1.4H13c-0.8,0-1.4-0.6-1.4-1.4v-0.4c0-0.8,0.6-1.4,1.4-1.4H33c0.8,0,1.4,0.6,1.4,1.4V35.6z" />
|
|
147
153
|
</svg>
|
|
148
154
|
);
|
|
149
155
|
case "harddisk":
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<path d="
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
+
// Storage — "storage" / "storage_outline".
|
|
157
|
+
return active ? (
|
|
158
|
+
<svg {...svgProps}>
|
|
159
|
+
<path d="M8,25.6H38c0.7,0,1.3-0.7,1.1-1.3L35.5,11c-0.4-1.4-1.7-2.3-3.2-2.3H13.7c-1.5,0-2.8,1-3.2,2.3L6.9,24.3C6.8,25,7.3,25.6,8,25.6z" />
|
|
160
|
+
<path d="M37.5,28h-29c-1.1,0-2,0.9-2,2v5.2c0,1.2,0.9,2.1,2.1,2.1h28.8c1.2,0,2.1-0.9,2.1-2.1V30C39.5,28.9,38.6,28,37.5,28z M18.1,33.3c0,0.5-0.4,0.9-0.9,0.9h-7.1c-0.5,0-0.9-0.4-0.9-0.9v-1.4c0-0.5,0.4-0.9,0.9-0.9h7.1c0.5,0,0.9,0.4,0.9,0.9V33.3z M30.3,34.2c-0.9,0-1.6-0.7-1.6-1.6s0.7-1.6,1.6-1.6s1.6,0.7,1.6,1.6S31.1,34.2,30.3,34.2z M35.4,34.2c-0.9,0-1.6-0.7-1.6-1.6s0.7-1.6,1.6-1.6s1.6,0.7,1.6,1.6S36.2,34.2,35.4,34.2z" />
|
|
161
|
+
</svg>
|
|
162
|
+
) : (
|
|
163
|
+
<svg {...svgProps}>
|
|
164
|
+
<path d="M9.1,25.7h27.8c0.7,0,1.4-0.3,1.8-0.9c0.4-0.5,0.5-1.2,0.4-1.8l-3.3-12.3c-0.5-1.8-2.2-3-4.1-3H14.4c-1.9,0-3.6,1.2-4.1,3L6.9,23c-0.2,0.6,0,1.3,0.4,1.8C7.7,25.3,8.4,25.7,9.1,25.7z M12.5,11.2c0.2-0.8,1-1.3,1.9-1.3h17.3c0.9,0,1.6,0.5,1.9,1.3l3.3,12.1H9.3L12.5,11.2z" />
|
|
165
|
+
<path d="M36.5,27.5h-27c-1.7,0-3,1.3-3,3v4.8c0,1.7,1.4,3.1,3.1,3.1h26.8c1.7,0,3.1-1.4,3.1-3.1v-4.8C39.5,28.8,38.2,27.5,36.5,27.5z M37.2,35.3c0,0.4-0.4,0.8-0.8,0.8H9.6c-0.4,0-0.8-0.4-0.8-0.8v-4.8c0-0.4,0.3-0.7,0.7-0.7h27c0.4,0,0.7,0.3,0.7,0.7V35.3z" />
|
|
166
|
+
<circle cx="34.6" cy="32.9" r="0.9" />
|
|
167
|
+
<circle cx="30.9" cy="32.9" r="0.9" />
|
|
168
|
+
<path d="M19.3,31.9h-8.5c-0.2,0-0.4,0.2-0.4,0.4v1c0,0.2,0.2,0.4,0.4,0.4h8.5c0.2,0,0.4-0.2,0.4-0.4v-1C19.7,32.1,19.6,31.9,19.3,31.9z" />
|
|
169
|
+
</svg>
|
|
170
|
+
);
|
|
171
|
+
case "database":
|
|
172
|
+
// Database — "database" / "database_outline".
|
|
173
|
+
return active ? (
|
|
174
|
+
<svg {...svgProps}>
|
|
175
|
+
<path d="M8.2,29.1v3.7c0,4.1,6.7,6.6,14.8,6.6l0.5,0c7.9-0.1,14.3-2.6,14.3-6.6v-3.7c-3.2,2.5-8.6,3.7-14.8,3.7C16.8,32.9,11.4,31.6,8.2,29.1z" />
|
|
176
|
+
<path d="M8.2,19.3V23l0,0.2l0,0.2c0.3,3.9,6.9,6.2,14.8,6.2c8.1,0,14.8-2.5,14.8-6.6v-3.7C34.5,21.8,29.2,23,23,23C16.8,23,11.4,21.7,8.2,19.3z" />
|
|
177
|
+
<path d="M23,19.7c8.1,0,14.8-2.5,14.8-6.6l0-0.3c0-0.4-0.2-0.8-0.3-1.2l-0.1-0.2c-0.1-0.1-0.1-0.2-0.2-0.3L37,10.8c-0.3-0.4-0.7-0.8-1.1-1.1c-0.2-0.1-0.3-0.2-0.5-0.4c-0.5-0.3-1-0.6-1.6-0.9l-0.3-0.1C33,8.1,32.6,8,32.1,7.8l-0.1,0l-0.7-0.2c-0.8-0.2-1.7-0.4-2.6-0.6l-0.8-0.1c-1.5-0.2-3.2-0.3-4.9-0.3l-0.5,0C14.8,6.7,8.6,9,8.2,12.8l0,0.2l0,0.2c0,0.1,0,0.2,0,0.3C8.6,17.4,15.1,19.7,23,19.7z" />
|
|
178
|
+
</svg>
|
|
179
|
+
) : (
|
|
180
|
+
<svg {...svgProps}>
|
|
181
|
+
<path d="M23,6.5c-7.4,0-14.8,2.2-14.8,6.3V23v10.2c0,4.2,7.4,6.3,14.8,6.3s14.8-2.2,14.8-6.3V23V12.8C37.8,8.7,30.4,6.5,23,6.5z M35.3,23c0,1.6-4.8,3.8-12.3,3.8S10.7,24.6,10.7,23v-6.5c2.8,1.7,7.6,2.7,12.3,2.7s9.5-0.9,12.3-2.7V23z M23,9c7.5,0,12.3,2.3,12.3,3.8s-4.8,3.8-12.3,3.8s-12.3-2.3-12.3-3.8S15.5,9,23,9z M23,37c-7.5,0-12.3-2.3-12.3-3.8v-6.5c2.8,1.7,7.6,2.7,12.3,2.7s9.5-0.9,12.3-2.7v6.5C35.3,34.7,30.5,37,23,37z" />
|
|
156
182
|
</svg>
|
|
157
183
|
);
|
|
158
184
|
case "code":
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
// Metadata — "metadata" / "metadata_outline" (a tag with a hole).
|
|
186
|
+
return active ? (
|
|
187
|
+
<svg {...svgProps}>
|
|
188
|
+
<path d="M21.6,6.5c1.3,0,2.6,0.5,3.5,1.5l12.7,12.7c2.2,2.2,2.2,5.8,0,8l-9.2,9.2c-2.2,2.2-5.8,2.2-8,0L7.9,25.1c-0.9-0.9-1.5-2.2-1.5-3.5v-8.5c0-3.6,3-6.6,6.6-6.6l0,0L21.6,6.5z M15.6,12.3c-1.7,0-3.2,1.3-3.3,3.1l0,0.2c0,1.8,1.5,3.3,3.3,3.3s3.3-1.5,3.3-3.3S17.4,12.3,15.6,12.3" />
|
|
189
|
+
</svg>
|
|
190
|
+
) : (
|
|
191
|
+
<svg {...svgProps}>
|
|
192
|
+
<path d="M15.4,12.4c-1.6,0-2.9,1.3-2.9,2.9c0,1.6,1.3,2.9,2.9,2.9s2.9-1.3,2.9-2.9C18.3,13.7,17,12.4,15.4,12.4z" />
|
|
193
|
+
<path d="M38,20.9L24.9,7.8c-0.9-0.9-2-1.4-3.3-1.4h-8.8c-3.5,0-6.3,2.8-6.3,6.3v8.8c0,1.2,0.5,2.4,1.4,3.3L20.9,38c1,1,2.3,1.6,3.8,1.6c1.4,0,2.8-0.6,3.8-1.6l9.5-9.5C40,26.4,40,23,38,20.9z M36.2,26.7l-9.5,9.5c-1.1,1.1-2.9,1.1-4,0L9.6,23.1C9.2,22.7,9,22.2,9,21.6v-8.8C9,10.7,10.7,9,12.8,9h8.8c0.6,0,1.1,0.2,1.5,0.6l13.1,13.1C37.3,23.8,37.3,25.6,36.2,26.7z" />
|
|
166
194
|
</svg>
|
|
167
195
|
);
|
|
168
196
|
case "cloud-download":
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<path d="
|
|
173
|
-
<path d="
|
|
197
|
+
// Backups — "backup" / "backup_outline".
|
|
198
|
+
return active ? (
|
|
199
|
+
<svg {...svgProps}>
|
|
200
|
+
<path d="M39.4,24.9c-0.2-3.4-2.9-6.2-6.3-6.4l-0.1,0l0-0.3c-0.1-2.5-1.3-4.8-3.4-6.5C27,9.6,23.4,9,20.1,10h0l-0.4,0.1C17.1,11,15,12.9,14,15.4l-0.1,0.3l-0.2,0c-4,0.6-7.1,4-7.1,8.1l0,0.3c0.2,4.3,3.7,7.7,8.1,7.9l0.1,0l0,0c-0.7-0.7-1.1-1.6-1.1-2.5c0-1,0.4-1.9,1.1-2.5c1.3-1.3,3.5-1.4,4.9-0.2v-2.8c0-1.9,1.6-3.5,3.5-3.5h0.2c1.9,0,3.5,1.6,3.5,3.5v2.8c1.2-1,2.8-1.1,4.1-0.4c1,0.6,1.6,1.5,1.7,2.6c0.1,1.1-0.2,2.2-1,3l-0.1,0.1h1.2l0.3,0c3.5-0.2,6.4-3.1,6.4-6.7L39.4,24.9z" />
|
|
201
|
+
<path d="M29.8,28.2c-0.5-0.3-1.2-0.2-1.7,0.3L24.6,32v-8.1c0-0.7-0.6-1.2-1.2-1.2h-0.2c-0.7,0-1.2,0.6-1.2,1.2V32l-3.6-3.6c-0.5-0.5-1.4-0.5-1.9,0C16.1,28.7,16,29,16,29.3c0,0.3,0.1,0.7,0.4,0.9l5.9,5.8c0.1,0.1,0.1,0.1,0.2,0.2c0,0,0,0,0.1,0c0.1,0,0.1,0.1,0.2,0.1c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0c0.1,0,0.1,0,0.2,0c0,0,0.1,0,0.1,0c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0.1,0c0.1,0,0.1-0.1,0.2-0.2l5.9-5.8C30.7,29.7,30.6,28.7,29.8,28.2z" />
|
|
202
|
+
</svg>
|
|
203
|
+
) : (
|
|
204
|
+
<svg {...svgProps}>
|
|
205
|
+
<path d="M29.8,28.3c-0.5-0.3-1.2-0.2-1.7,0.3l-3.6,3.5v-8.1c0-0.7-0.6-1.2-1.2-1.2h-0.2c-0.7,0-1.2,0.6-1.2,1.2v8.1l-3.6-3.6c-0.5-0.5-1.4-0.5-1.9,0c-0.3,0.3-0.4,0.6-0.4,0.9c0,0.3,0.1,0.7,0.4,0.9l5.9,5.8c0.1,0.1,0.1,0.1,0.2,0.2c0,0,0,0,0.1,0c0.1,0,0.1,0.1,0.2,0.1c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0c0.1,0,0.1,0,0.2,0c0,0,0.1,0,0.1,0c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0.1,0c0.1,0,0.1-0.1,0.2-0.2l5.9-5.8C30.7,29.7,30.6,28.7,29.8,28.3z" />
|
|
206
|
+
<path d="M33,18.7h-0.2c0.1-2.7-1.1-5.3-3.3-7c-2.6-2.1-6.2-2.7-9.4-1.7c-3,0.9-5.3,3.1-6.2,5.8c-4.2,0.4-7.4,3.9-7.4,8c0,4,3,7.3,6.9,7.9c0.5,0.1,0.9-0.5,0.6-0.9c-0.4-0.6-0.9-1.3-1.1-1.5c-0.1-0.1-0.2-0.2-0.3-0.2c-2.2-0.8-3.8-2.9-3.8-5.3c0-3.1,2.7-5.7,6-5.7c0.6,0,1-0.4,1.1-0.9c0.5-2.3,2.4-4.2,4.8-4.9c2.5-0.8,5.3-0.3,7.3,1.3c1.9,1.5,2.8,3.9,2.3,6.1c-0.1,0.3,0,0.7,0.2,1s0.6,0.4,0.9,0.4H33c2.3,0,4.1,1.9,4.1,4.2c0,2.2-1.7,4-3.8,4.1c-0.2,0-0.4,0.1-0.5,0.3l-1.3,2.1H33c3.6,0,6.5-2.9,6.5-6.5C39.5,21.6,36.6,18.7,33,18.7z" />
|
|
174
207
|
</svg>
|
|
175
208
|
);
|
|
176
209
|
case "setup":
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
// Setup Wizard — "wizard" / "wizard_outline".
|
|
211
|
+
return active ? (
|
|
212
|
+
<svg {...svgProps}>
|
|
213
|
+
<path d="M27.4,9.9c0.2,0.6,0.8,1,1.4,0.9c0.1,0,0.2-0.1,0.3-0.1c4-2.5,8.6,2.2,6.2,6.2c-0.3,0.6-0.2,1.3,0.4,1.6c0.1,0.1,0.2,0.1,0.3,0.1c4.6,1.1,4.6,7.6,0,8.7c-0.6,0.2-1,0.8-0.9,1.4c0,0.1,0.1,0.2,0.1,0.3c2.5,4-2.2,8.6-6.2,6.2c-0.6-0.3-1.3-0.2-1.6,0.4c-0.1,0.1-0.1,0.2-0.1,0.3c-1.1,4.6-7.6,4.6-8.7,0c-0.2-0.6-0.8-1-1.4-0.9c-0.1,0-0.2,0.1-0.3,0.1c-4,2.5-8.6-2.2-6.2-6.2c0.3-0.6,0.2-1.3-0.4-1.6c-0.1-0.1-0.2-0.1-0.3-0.1c-4.6-1.1-4.6-7.6,0-8.7c0.6-0.2,1-0.8,0.9-1.4c0-0.1-0.1-0.2-0.1-0.3c-2.5-4,2.2-8.6,6.2-6.2c0.6,0.3,1.3,0.2,1.6-0.4c0.1-0.1,0.1-0.2,0.1-0.3C19.7,5.4,26.3,5.4,27.4,9.9z M23,18.1c-2.7,0-4.9,2.2-4.9,4.9s2.2,4.9,4.9,4.9s4.9-2.2,4.9-4.9S25.7,18.1,23,18.1" />
|
|
214
|
+
</svg>
|
|
215
|
+
) : (
|
|
216
|
+
<svg {...svgProps}>
|
|
217
|
+
<path d="M36.3,19c-0.2,0-0.3-0.1-0.5-0.2c-0.4-0.2-0.7-0.6-0.8-1.1c-0.1-0.4,0-0.9,0.2-1.3c1-1.7,0.8-3.7-0.6-5.1c-1.4-1.4-3.4-1.6-5.1-0.6c-0.1,0.1-0.3,0.2-0.5,0.2c-0.4,0.1-0.9,0-1.3-0.2c-0.4-0.2-0.7-0.6-0.8-1.1c-0.5-1.9-2.1-3.2-4-3.2c-2,0-3.6,1.3-4,3.2c-0.1,0.5-0.5,1-1,1.2c-0.5,0.2-1.1,0.2-1.5-0.1c-1.7-1-3.7-0.8-5.1,0.6c-1.4,1.4-1.6,3.4-0.6,5.1c0.1,0.2,0.2,0.3,0.2,0.5c0.2,0.9-0.3,1.8-1.3,2.1c-1.9,0.5-3.2,2.1-3.2,4c0,2,1.3,3.6,3.2,4c0.2,0,0.3,0.1,0.5,0.2c0.8,0.5,1.1,1.5,0.6,2.3c-1,1.7-0.8,3.7,0.6,5.1c1.4,1.4,3.4,1.6,5.1,0.6c0.2-0.1,0.3-0.2,0.5-0.2c0.9-0.2,1.8,0.3,2.1,1.3c0.5,1.9,2.1,3.2,4,3.2s3.6-1.3,4-3.2c0-0.2,0.1-0.3,0.2-0.5c0.2-0.4,0.6-0.7,1.1-0.8c0.4-0.1,0.9,0,1.3,0.2c1.7,1,3.7,0.8,5.1-0.6c1.4-1.4,1.6-3.4,0.6-5.1c-0.1-0.2-0.2-0.3-0.2-0.5c-0.1-0.4,0-0.9,0.2-1.3c0.2-0.4,0.6-0.7,1.1-0.8c1.9-0.5,3.2-2.1,3.2-4C39.5,21,38.2,19.4,36.3,19z M35.8,24.7c-1.1,0.3-2,0.9-2.6,1.9c-0.6,0.9-0.8,2.1-0.5,3.1c0.1,0.4,0.3,0.8,0.5,1.2c0.5,0.8,0.2,1.6-0.3,2.1c-0.5,0.5-1.2,0.8-2.1,0.3c-0.9-0.6-2.1-0.7-3.1-0.5c-1.1,0.3-2,0.9-2.6,1.9c-0.2,0.4-0.4,0.8-0.5,1.2c-0.2,1-1,1.3-1.7,1.3s-1.4-0.3-1.7-1.3c-0.5-1.9-2.2-3.2-4-3.2c-0.3,0-0.7,0-1,0.1c-0.4,0.1-0.8,0.3-1.2,0.5c-0.8,0.5-1.6,0.2-2.1-0.3c-0.5-0.5-0.8-1.2-0.3-2.1c1.2-2,0.6-4.5-1.4-5.7c-0.4-0.2-0.8-0.4-1.2-0.5c-1-0.2-1.3-1-1.3-1.7c0-0.6,0.3-1.4,1.3-1.7c2.2-0.5,3.6-2.8,3.1-5c-0.1-0.4-0.3-0.8-0.5-1.2c-0.5-0.8-0.2-1.6,0.3-2.1c0.5-0.5,1.2-0.8,2.1-0.3c1.1,0.7,2.5,0.8,3.7,0.3c1.2-0.5,2.1-1.6,2.4-2.9c0,0,0,0,0,0c0.2-1,1-1.3,1.7-1.3s1.4,0.3,1.7,1.3c0.5,2.2,2.8,3.6,5,3.1c0.4-0.1,0.8-0.3,1.2-0.5c0.8-0.5,1.6-0.2,2.1,0.3c0.5,0.5,0.8,1.2,0.3,2.1c-0.6,0.9-0.8,2.1-0.5,3.1c0.3,1.1,0.9,2,1.9,2.6c0.4,0.2,0.8,0.4,1.2,0.5c1,0.2,1.3,1,1.3,1.7C37.1,23.6,36.7,24.4,35.8,24.7z" />
|
|
218
|
+
<path d="M23,16.7c-3.5,0-6.3,2.8-6.3,6.3s2.8,6.3,6.3,6.3s6.3-2.8,6.3-6.3S26.5,16.7,23,16.7z M23,26.9c-2.1,0-3.9-1.7-3.9-3.9s1.7-3.9,3.9-3.9s3.9,1.7,3.9,3.9S25.1,26.9,23,26.9z" />
|
|
180
219
|
</svg>
|
|
181
220
|
);
|
|
182
221
|
case "view":
|
|
222
|
+
// View Site — outline only ("view_site"); opens the public site in a
|
|
223
|
+
// new tab, no admin route to highlight.
|
|
183
224
|
return (
|
|
184
|
-
<svg
|
|
185
|
-
<path d="
|
|
186
|
-
<path d="
|
|
187
|
-
<path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
|
|
225
|
+
<svg {...svgProps}>
|
|
226
|
+
<path d="M39.4,17.3l-0.1-10l-0.2-0.3c0,0-0.1-0.1-0.1-0.1l-0.4-0.3l-0.6-0.1h-9.4c-0.8,0-1.4,0.6-1.4,1.4s0.6,1.4,1.4,1.4h6.1L20.1,23.9c-0.5,0.5-0.5,1.4,0,1.9c0.5,0.5,1.4,0.6,1.9,0l14.6-14.6v6.1c0,0.8,0.6,1.4,1.4,1.4C38.8,18.7,39.4,18.1,39.4,17.3z" />
|
|
227
|
+
<path d="M23,10.3H11.7c-2.8,0-5.1,2.3-5.1,5.1v18.8c0,2.8,2.3,5.1,5.1,5.1h18.8c2.8,0,5.1-2.3,5.1-5.1V23c0-0.8-0.6-1.4-1.4-1.4c-0.8,0-1.4,0.6-1.4,1.4v11.3c0,1.3-1.1,2.4-2.4,2.4H11.7c-1.3,0-2.4-1.1-2.4-2.4V15.5c0-1.3,1.1-2.4,2.4-2.4H23c0.8,0,1.4-0.6,1.4-1.4S23.8,10.3,23,10.3z" />
|
|
188
228
|
</svg>
|
|
189
229
|
);
|
|
190
230
|
case "logout":
|
|
231
|
+
// Log out — outline only ("exit"); button only, no admin route.
|
|
191
232
|
return (
|
|
192
|
-
<svg
|
|
193
|
-
<path d="
|
|
194
|
-
<
|
|
195
|
-
<line x1="21" y1="12" x2="9" y2="12" />
|
|
233
|
+
<svg {...svgProps}>
|
|
234
|
+
<path d="M39.2,22.6l-0.1-0.4l-0.3-0.5l-5.3-5.3c-0.5-0.5-1.4-0.5-1.9,0c-0.5,0.5-0.5,1.4,0,1.9l3,3l-18.6,0c-0.7,0-1.3,0.6-1.3,1.3c0,0.7,0.6,1.3,1.3,1.4h18.6l-3,3c-0.5,0.5-0.5,1.4,0,1.9c0.3,0.3,0.6,0.4,1,0.4c0.3,0,0.7-0.1,1-0.4l5.6-5.7l0.1-0.4C39.2,22.8,39.2,22.7,39.2,22.6z" />
|
|
235
|
+
<path d="M38.2,30c-0.7,0-1.3,0.6-1.3,1.3v2.5c0,1.7-1.4,3-3,3l-21.7,0c-1.7,0-3-1.3-3-3V12.2c0-1.7,1.4-3,3-3l21.7,0c1.7,0,3,1.3,3,3v2.5c0,0.7,0.6,1.3,1.3,1.3c0.7,0,1.3-0.6,1.3-1.3v-2.5c0-3.1-2.5-5.6-5.7-5.6H12.2c-0.4,0-0.8,0-1.1,0.1c-2.6,0.5-4.5,2.9-4.5,5.5v21.6c0,2.3,1.4,4.3,3.5,5.2c0.7,0.3,1.4,0.4,2.2,0.4h21.7c3.1,0,5.7-2.5,5.7-5.6v-2.5C39.5,30.6,38.9,30,38.2,30z" />
|
|
196
236
|
</svg>
|
|
197
237
|
);
|
|
198
238
|
default:
|
|
@@ -245,7 +285,7 @@ export default function AdminLayout({
|
|
|
245
285
|
}`}
|
|
246
286
|
aria-label={link.label}
|
|
247
287
|
>
|
|
248
|
-
<NavIcon icon={link.icon} />
|
|
288
|
+
<NavIcon icon={link.icon} active={isActive} />
|
|
249
289
|
<Tooltip>{link.label}</Tooltip>
|
|
250
290
|
</Link>
|
|
251
291
|
);
|
|
@@ -265,7 +305,7 @@ export default function AdminLayout({
|
|
|
265
305
|
className={`${tileBase} bg-white`}
|
|
266
306
|
aria-label={`Morphika Andami v${ANDAMI_VERSION}`}
|
|
267
307
|
>
|
|
268
|
-
<AndamiMark size={
|
|
308
|
+
<AndamiMark size={24} />
|
|
269
309
|
<Tooltip>
|
|
270
310
|
Morphika Andami
|
|
271
311
|
<span className="ml-1.5 text-white/55">v{ANDAMI_VERSION}</span>
|
|
@@ -288,10 +328,14 @@ export default function AdminLayout({
|
|
|
288
328
|
|
|
289
329
|
<Link
|
|
290
330
|
href="/admin/setup"
|
|
291
|
-
className={`${tileBase}
|
|
331
|
+
className={`${tileBase} ${
|
|
332
|
+
isLinkActive("/admin/setup")
|
|
333
|
+
? "bg-[#3580f9] text-white"
|
|
334
|
+
: "text-white/55 hover:bg-white/[0.06] hover:text-white"
|
|
335
|
+
}`}
|
|
292
336
|
aria-label="Setup Wizard"
|
|
293
337
|
>
|
|
294
|
-
<NavIcon icon="setup" />
|
|
338
|
+
<NavIcon icon="setup" active={isLinkActive("/admin/setup")} />
|
|
295
339
|
<Tooltip>Setup Wizard</Tooltip>
|
|
296
340
|
</Link>
|
|
297
341
|
|
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
TypographyEditor,
|
|
12
12
|
ColorsEditor,
|
|
13
13
|
LinksButtonsEditor,
|
|
14
|
+
CursorEditor,
|
|
14
15
|
} from "../../../components/admin/styles";
|
|
15
16
|
|
|
16
17
|
// ============================================
|
|
17
18
|
// Tab definitions
|
|
18
19
|
// ============================================
|
|
19
20
|
|
|
20
|
-
type TabId = "grid" | "typography" | "colors" | "buttons";
|
|
21
|
+
type TabId = "grid" | "typography" | "colors" | "buttons" | "cursor";
|
|
21
22
|
|
|
22
23
|
function TabIcon({ id, size = 15 }: { id: TabId; size?: number }) {
|
|
23
24
|
const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none" as const, stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
|
|
@@ -56,6 +57,12 @@ function TabIcon({ id, size = 15 }: { id: TabId; size?: number }) {
|
|
|
56
57
|
<line x1="8" y1="12" x2="16" y2="12" />
|
|
57
58
|
</svg>
|
|
58
59
|
);
|
|
60
|
+
case "cursor":
|
|
61
|
+
return (
|
|
62
|
+
<svg {...props}>
|
|
63
|
+
<path d="M5 3l6 16 2-7 7-2z" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -64,6 +71,7 @@ const TABS: { id: TabId; label: string }[] = [
|
|
|
64
71
|
{ id: "typography", label: "Typography" },
|
|
65
72
|
{ id: "colors", label: "Colors" },
|
|
66
73
|
{ id: "buttons", label: "Buttons & Links" },
|
|
74
|
+
{ id: "cursor", label: "Mouse" },
|
|
67
75
|
];
|
|
68
76
|
|
|
69
77
|
// ============================================
|
|
@@ -238,6 +246,14 @@ export default function AdminStylesPage() {
|
|
|
238
246
|
saving={saving === "links"}
|
|
239
247
|
/>
|
|
240
248
|
)}
|
|
249
|
+
|
|
250
|
+
{activeTab === "cursor" && (
|
|
251
|
+
<CursorEditor
|
|
252
|
+
cursor={styles?.cursor}
|
|
253
|
+
onSave={(data) => saveSection("cursor", data)}
|
|
254
|
+
saving={saving === "cursor"}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
241
257
|
</div>
|
|
242
258
|
);
|
|
243
259
|
}
|
|
@@ -137,6 +137,11 @@ export async function GET() {
|
|
|
137
137
|
secondary_text: styles.button_secondary_text || "#ffffff",
|
|
138
138
|
border_radius: styles.button_border_radius || "8px",
|
|
139
139
|
},
|
|
140
|
+
cursor: {
|
|
141
|
+
color: styles.cursor_color || "#3580f9",
|
|
142
|
+
opacity: typeof styles.cursor_opacity === "number" ? styles.cursor_opacity : 100,
|
|
143
|
+
show_labels: !!styles.cursor_show_labels,
|
|
144
|
+
},
|
|
140
145
|
disable_scroll_animations_mobile: styles.disable_scroll_animations_mobile ?? false,
|
|
141
146
|
};
|
|
142
147
|
|
|
@@ -176,7 +181,7 @@ export async function POST(request: NextRequest) {
|
|
|
176
181
|
);
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
const validSections = ["grid", "fonts", "typography", "colors", "links"];
|
|
184
|
+
const validSections = ["grid", "fonts", "typography", "colors", "links", "cursor", "animations"];
|
|
180
185
|
if (!validSections.includes(section)) {
|
|
181
186
|
return NextResponse.json(
|
|
182
187
|
{ error: `Invalid section: ${section}` },
|
|
@@ -286,6 +291,18 @@ export async function POST(request: NextRequest) {
|
|
|
286
291
|
}
|
|
287
292
|
break;
|
|
288
293
|
}
|
|
294
|
+
|
|
295
|
+
case "cursor": {
|
|
296
|
+
if (data.cursor_color !== undefined) patch.cursor_color = String(data.cursor_color);
|
|
297
|
+
if (data.cursor_opacity !== undefined) {
|
|
298
|
+
const n = Number(data.cursor_opacity);
|
|
299
|
+
patch.cursor_opacity = Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : 100;
|
|
300
|
+
}
|
|
301
|
+
if (data.cursor_show_labels !== undefined) {
|
|
302
|
+
patch.cursor_show_labels = !!data.cursor_show_labels;
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
// Ensure the document exists
|
package/app/api/styles/route.ts
CHANGED
|
@@ -70,6 +70,11 @@ export async function GET() {
|
|
|
70
70
|
secondary_text: raw.button_secondary_text || "#ffffff",
|
|
71
71
|
border_radius: raw.button_border_radius || "8px",
|
|
72
72
|
},
|
|
73
|
+
cursor: {
|
|
74
|
+
color: raw.cursor_color || "#3580f9",
|
|
75
|
+
opacity: typeof raw.cursor_opacity === "number" ? raw.cursor_opacity : 100,
|
|
76
|
+
show_labels: !!raw.cursor_show_labels,
|
|
77
|
+
},
|
|
73
78
|
disable_scroll_animations_mobile: raw.disable_scroll_animations_mobile ?? false,
|
|
74
79
|
};
|
|
75
80
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import type { CursorSettings } from "../../../lib/sanity/types";
|
|
5
|
+
import { Section, SaveButton, ColorField } from "./shared";
|
|
6
|
+
|
|
7
|
+
export function CursorEditor({
|
|
8
|
+
cursor,
|
|
9
|
+
onSave,
|
|
10
|
+
saving,
|
|
11
|
+
}: {
|
|
12
|
+
cursor?: CursorSettings;
|
|
13
|
+
onSave: (data: Record<string, unknown>) => void;
|
|
14
|
+
saving: boolean;
|
|
15
|
+
}) {
|
|
16
|
+
const [local, setLocal] = useState({
|
|
17
|
+
cursor_color: cursor?.color || "#3580f9",
|
|
18
|
+
cursor_opacity: typeof cursor?.opacity === "number" ? cursor.opacity : 100,
|
|
19
|
+
cursor_show_labels: !!cursor?.show_labels,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setLocal({
|
|
24
|
+
cursor_color: cursor?.color || "#3580f9",
|
|
25
|
+
cursor_opacity: typeof cursor?.opacity === "number" ? cursor.opacity : 100,
|
|
26
|
+
cursor_show_labels: !!cursor?.show_labels,
|
|
27
|
+
});
|
|
28
|
+
}, [cursor]);
|
|
29
|
+
|
|
30
|
+
const previewAlpha = Math.max(0, Math.min(100, local.cursor_opacity)) / 100;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Section
|
|
34
|
+
title="Mouse Cursor"
|
|
35
|
+
description="Custom cursor that replaces the system pointer on the public site. Activated globally via site.config.ts → features.customCursor."
|
|
36
|
+
>
|
|
37
|
+
<div className="grid grid-cols-2 gap-6 mb-6">
|
|
38
|
+
<ColorField
|
|
39
|
+
label="Cursor Color"
|
|
40
|
+
value={local.cursor_color}
|
|
41
|
+
onChange={(v) => setLocal({ ...local, cursor_color: v })}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<div>
|
|
45
|
+
<label className="text-xs text-neutral-500 block mb-1">
|
|
46
|
+
Opacity — {local.cursor_opacity}%
|
|
47
|
+
</label>
|
|
48
|
+
<input
|
|
49
|
+
type="range"
|
|
50
|
+
min={0}
|
|
51
|
+
max={100}
|
|
52
|
+
step={1}
|
|
53
|
+
value={local.cursor_opacity}
|
|
54
|
+
onChange={(e) =>
|
|
55
|
+
setLocal({ ...local, cursor_opacity: Number(e.target.value) })
|
|
56
|
+
}
|
|
57
|
+
className="w-full accent-[#3580f9]"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<label className="flex items-start gap-3 mb-6 cursor-pointer">
|
|
63
|
+
<input
|
|
64
|
+
type="checkbox"
|
|
65
|
+
checked={local.cursor_show_labels}
|
|
66
|
+
onChange={(e) =>
|
|
67
|
+
setLocal({ ...local, cursor_show_labels: e.target.checked })
|
|
68
|
+
}
|
|
69
|
+
className="mt-0.5 accent-[#3580f9]"
|
|
70
|
+
/>
|
|
71
|
+
<div>
|
|
72
|
+
<span className="text-sm text-neutral-800 block">Show hover labels</span>
|
|
73
|
+
<span className="text-xs text-neutral-500 block mt-0.5">
|
|
74
|
+
Adds <em>Zoom</em> on images, <em>Play</em> on videos, and <em>View</em> on links when hovered.
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</label>
|
|
78
|
+
|
|
79
|
+
{/* Preview */}
|
|
80
|
+
<div className="mb-5 p-6 rounded-lg bg-neutral-900 flex items-center justify-center gap-6">
|
|
81
|
+
<span className="text-xs text-neutral-400">Preview:</span>
|
|
82
|
+
<div
|
|
83
|
+
className="rounded-full border-[1.5px] flex items-center justify-center"
|
|
84
|
+
style={{
|
|
85
|
+
width: local.cursor_show_labels ? 60 : 38,
|
|
86
|
+
height: local.cursor_show_labels ? 60 : 38,
|
|
87
|
+
borderColor: local.cursor_color,
|
|
88
|
+
opacity: previewAlpha,
|
|
89
|
+
transition: "width 0.2s ease, height 0.2s ease",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{local.cursor_show_labels && (
|
|
93
|
+
<span
|
|
94
|
+
className="text-[10px] font-medium uppercase tracking-wider"
|
|
95
|
+
style={{ color: local.cursor_color }}
|
|
96
|
+
>
|
|
97
|
+
Zoom
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex justify-end">
|
|
104
|
+
<SaveButton onClick={() => onSave(local)} saving={saving} />
|
|
105
|
+
</div>
|
|
106
|
+
</Section>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -154,7 +154,11 @@ export default function ParallaxSlideRenderer({
|
|
|
154
154
|
</div>
|
|
155
155
|
)}
|
|
156
156
|
|
|
157
|
-
{/* ── Content layer — full V2 section ──
|
|
157
|
+
{/* ── Content layer — full V2 section ──
|
|
158
|
+
fillHeight stretches the V2 grid to 100% of the slide so each column's
|
|
159
|
+
`align_v` (top / center / bottom) has room to apply. No outer
|
|
160
|
+
justify-content here — the column owns vertical alignment, mirroring
|
|
161
|
+
ParallaxGroupCanvas in the builder. */}
|
|
158
162
|
<div
|
|
159
163
|
style={{
|
|
160
164
|
position: "relative",
|
|
@@ -162,12 +166,12 @@ export default function ParallaxSlideRenderer({
|
|
|
162
166
|
height: "100%",
|
|
163
167
|
display: "flex",
|
|
164
168
|
flexDirection: "column",
|
|
165
|
-
justifyContent: "center",
|
|
166
169
|
}}
|
|
167
170
|
>
|
|
168
171
|
<SectionV2Renderer
|
|
169
172
|
section={pseudoSection}
|
|
170
173
|
pageEnterAnimation={pageEnterAnimation}
|
|
174
|
+
fillHeight
|
|
171
175
|
/>
|
|
172
176
|
</div>
|
|
173
177
|
</section>
|
|
@@ -191,9 +191,16 @@ interface SectionV2RendererProps {
|
|
|
191
191
|
section: PageSectionV2;
|
|
192
192
|
/** Page-level enter animation config (from page_settings.enter_animation) */
|
|
193
193
|
pageEnterAnimation?: EnterAnimationConfig;
|
|
194
|
+
/**
|
|
195
|
+
* Stretch the section + grid to 100% of the parent's height so column-level
|
|
196
|
+
* `align_v` (top/center/bottom) has room to apply. Used by parallax slides
|
|
197
|
+
* and cover sections where the slide/row enforces a fixed height. Mirrors
|
|
198
|
+
* `fillHeight` in SectionV2Canvas (builder side).
|
|
199
|
+
*/
|
|
200
|
+
fillHeight?: boolean;
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
export default function SectionV2Renderer({ section, pageEnterAnimation }: SectionV2RendererProps) {
|
|
203
|
+
export default function SectionV2Renderer({ section, pageEnterAnimation, fillHeight }: SectionV2RendererProps) {
|
|
197
204
|
const s = section.settings;
|
|
198
205
|
|
|
199
206
|
const gridColumns = s.grid_columns || 12;
|
|
@@ -236,7 +243,10 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
236
243
|
const sectionContent = (
|
|
237
244
|
<section
|
|
238
245
|
className={`sv2-${section._key}`}
|
|
239
|
-
style={
|
|
246
|
+
style={{
|
|
247
|
+
...layoutStyles,
|
|
248
|
+
...(fillHeight ? { display: "flex", flexDirection: "column", height: "100%" } : {}),
|
|
249
|
+
}}
|
|
240
250
|
>
|
|
241
251
|
{responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
|
|
242
252
|
<div
|
|
@@ -245,6 +255,7 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
245
255
|
marginLeft: "auto",
|
|
246
256
|
marginRight: "auto",
|
|
247
257
|
width: "100%",
|
|
258
|
+
...(fillHeight ? { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } : {}),
|
|
248
259
|
}}
|
|
249
260
|
>
|
|
250
261
|
<div
|
|
@@ -253,6 +264,7 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
253
264
|
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
|
254
265
|
columnGap: `${colGap}px`,
|
|
255
266
|
rowGap: `${rowGap}px`,
|
|
267
|
+
...(fillHeight ? { flex: 1, minHeight: 0, gridTemplateRows: "minmax(0, 1fr)" } : {}),
|
|
256
268
|
}}
|
|
257
269
|
>
|
|
258
270
|
{sortedColumns.map((col, colIndex) => {
|
|
@@ -1,118 +1,138 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import { useSiteStyles } from "../../lib/styles/provider";
|
|
5
|
+
|
|
6
|
+
/** Map a hovered element to a hover label, or null if no label applies. */
|
|
7
|
+
function getHoverLabel(target: HTMLElement | null): string | null {
|
|
8
|
+
if (!target) return null;
|
|
9
|
+
// Honour explicit data-cursor-label override (e.g. data-cursor-label="Open")
|
|
10
|
+
const explicit = target.closest<HTMLElement>("[data-cursor-label]");
|
|
11
|
+
if (explicit) return explicit.dataset.cursorLabel || null;
|
|
12
|
+
if (target.closest("img, [data-cursor-context='image']")) return "Zoom";
|
|
13
|
+
if (target.closest("video, [data-cursor-context='video']")) return "Play";
|
|
14
|
+
if (target.closest("a")) return "View";
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Hover-target detection — anything interactive triggers the expanded state. */
|
|
19
|
+
const HOVER_SELECTOR =
|
|
20
|
+
"a, button, [role='button'], input, textarea, select, img, video, [data-cursor-hover]";
|
|
21
|
+
|
|
22
|
+
export default function CustomCursor() {
|
|
23
|
+
const styles = useSiteStyles();
|
|
24
|
+
const cursor = styles?.cursor;
|
|
25
|
+
const color = cursor?.color || "#3580f9";
|
|
26
|
+
const opacity = (typeof cursor?.opacity === "number" ? cursor.opacity : 100) / 100;
|
|
27
|
+
const showLabels = !!cursor?.show_labels;
|
|
28
|
+
|
|
29
|
+
const [position, setPosition] = useState({ x: -100, y: -100 });
|
|
30
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
31
|
+
const [label, setLabel] = useState<string | null>(null);
|
|
32
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
33
|
+
const [isMobile, setIsMobile] = useState(true);
|
|
34
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
35
|
+
const rafId = useRef<number>(0);
|
|
36
|
+
|
|
37
|
+
// Check if device supports hover (desktop)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const mql = window.matchMedia("(hover: hover) and (pointer: fine)");
|
|
40
|
+
setIsMobile(!mql.matches);
|
|
41
|
+
|
|
42
|
+
const handler = (e: MediaQueryListEvent) => setIsMobile(!e.matches);
|
|
43
|
+
mql.addEventListener("change", handler);
|
|
44
|
+
return () => mql.removeEventListener("change", handler);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
// Respect prefers-reduced-motion — disable custom cursor entirely
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
50
|
+
setPrefersReducedMotion(mql.matches);
|
|
51
|
+
|
|
52
|
+
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
|
|
53
|
+
mql.addEventListener("change", handler);
|
|
54
|
+
return () => mql.removeEventListener("change", handler);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
58
|
+
if (rafId.current) cancelAnimationFrame(rafId.current);
|
|
59
|
+
rafId.current = requestAnimationFrame(() => {
|
|
60
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
61
|
+
setIsVisible(true);
|
|
62
|
+
});
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleMouseLeave = useCallback(() => {
|
|
66
|
+
setIsVisible(false);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Track hover state on interactive elements + resolve contextual label
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isMobile) return;
|
|
72
|
+
|
|
73
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
74
|
+
const target = e.target as HTMLElement;
|
|
75
|
+
if (target.closest(HOVER_SELECTOR)) {
|
|
76
|
+
setIsHovering(true);
|
|
77
|
+
if (showLabels) setLabel(getHoverLabel(target));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleMouseOut = (e: MouseEvent) => {
|
|
82
|
+
const target = e.target as HTMLElement;
|
|
83
|
+
if (target.closest(HOVER_SELECTOR)) {
|
|
84
|
+
setIsHovering(false);
|
|
85
|
+
setLabel(null);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
90
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
91
|
+
document.addEventListener("mouseover", handleMouseOver);
|
|
92
|
+
document.addEventListener("mouseout", handleMouseOut);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
96
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
97
|
+
document.removeEventListener("mouseover", handleMouseOver);
|
|
98
|
+
document.removeEventListener("mouseout", handleMouseOut);
|
|
99
|
+
if (rafId.current) cancelAnimationFrame(rafId.current);
|
|
100
|
+
};
|
|
101
|
+
}, [isMobile, showLabels, handleMouseMove, handleMouseLeave]);
|
|
102
|
+
|
|
103
|
+
if (isMobile || prefersReducedMotion) return null;
|
|
104
|
+
|
|
105
|
+
// Expanded size when over an interactive element. When labels are on and
|
|
106
|
+
// a label is present, we expand further to fit the text comfortably.
|
|
107
|
+
const showLabelNow = showLabels && isHovering && !!label;
|
|
108
|
+
const size = showLabelNow ? 60 : isHovering ? 40 : 20;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
className="pointer-events-none fixed inset-0 z-[9999]"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
>
|
|
115
|
+
<div
|
|
116
|
+
className="absolute rounded-full border-[1.5px] flex items-center justify-center"
|
|
117
|
+
style={{
|
|
118
|
+
width: size,
|
|
119
|
+
height: size,
|
|
120
|
+
left: position.x - size / 2,
|
|
121
|
+
top: position.y - size / 2,
|
|
122
|
+
borderColor: color,
|
|
123
|
+
opacity: isVisible ? opacity : 0,
|
|
124
|
+
transition: "width 0.2s ease, height 0.2s ease, opacity 0.15s ease",
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{showLabelNow && (
|
|
128
|
+
<span
|
|
129
|
+
className="text-[10px] font-medium uppercase tracking-wider select-none"
|
|
130
|
+
style={{ color }}
|
|
131
|
+
>
|
|
132
|
+
{label}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
package/lib/sanity/queries.ts
CHANGED
|
@@ -381,7 +381,10 @@ export const siteStylesQuery = groq`
|
|
|
381
381
|
button_secondary_bg,
|
|
382
382
|
button_secondary_text,
|
|
383
383
|
button_border_radius,
|
|
384
|
-
disable_scroll_animations_mobile
|
|
384
|
+
disable_scroll_animations_mobile,
|
|
385
|
+
cursor_color,
|
|
386
|
+
cursor_opacity,
|
|
387
|
+
cursor_show_labels
|
|
385
388
|
}
|
|
386
389
|
`;
|
|
387
390
|
|
package/lib/sanity/types.ts
CHANGED
|
@@ -1163,6 +1163,14 @@ export interface GridSettings {
|
|
|
1163
1163
|
gutter_phone?: string; // e.g. "16" (phone, ≤640px)
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
1166
|
+
export interface CursorSettings {
|
|
1167
|
+
color: string;
|
|
1168
|
+
/** 0–100 */
|
|
1169
|
+
opacity: number;
|
|
1170
|
+
/** Show 'Zoom'/'Play'/'View' labels on hover over images/videos/links */
|
|
1171
|
+
show_labels: boolean;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1166
1174
|
export interface SiteStyles {
|
|
1167
1175
|
grid?: GridSettings;
|
|
1168
1176
|
fonts?: FontFamily[];
|
|
@@ -1177,6 +1185,7 @@ export interface SiteStyles {
|
|
|
1177
1185
|
colors?: ColorPalette;
|
|
1178
1186
|
link_style?: LinkStyle;
|
|
1179
1187
|
button_style?: ButtonStyle;
|
|
1188
|
+
cursor?: CursorSettings;
|
|
1180
1189
|
/** When true, disables all scroll animations on viewports ≤768px */
|
|
1181
1190
|
disable_scroll_animations_mobile?: boolean;
|
|
1182
1191
|
}
|
package/lib/version.ts
CHANGED
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ export default defineType({
|
|
|
13
13
|
{ name: "typography", title: "Typography" },
|
|
14
14
|
{ name: "colors", title: "Colors" },
|
|
15
15
|
{ name: "links", title: "Links & Buttons" },
|
|
16
|
+
{ name: "cursor", title: "Mouse" },
|
|
16
17
|
],
|
|
17
18
|
fields: [
|
|
18
19
|
// === GRID ===
|
|
@@ -208,5 +209,31 @@ export default defineType({
|
|
|
208
209
|
description: "When enabled, all scroll-linked animations are disabled on phones (< 768px)",
|
|
209
210
|
initialValue: false,
|
|
210
211
|
}),
|
|
212
|
+
|
|
213
|
+
// === CURSOR (custom mouse cursor) ===
|
|
214
|
+
defineField({
|
|
215
|
+
name: "cursor_color",
|
|
216
|
+
title: "Cursor Color",
|
|
217
|
+
type: "string",
|
|
218
|
+
group: "cursor",
|
|
219
|
+
initialValue: "#3580f9",
|
|
220
|
+
}),
|
|
221
|
+
defineField({
|
|
222
|
+
name: "cursor_opacity",
|
|
223
|
+
title: "Cursor Opacity",
|
|
224
|
+
type: "number",
|
|
225
|
+
group: "cursor",
|
|
226
|
+
description: "0–100",
|
|
227
|
+
initialValue: 100,
|
|
228
|
+
validation: (Rule) => Rule.min(0).max(100),
|
|
229
|
+
}),
|
|
230
|
+
defineField({
|
|
231
|
+
name: "cursor_show_labels",
|
|
232
|
+
title: "Show Hover Labels",
|
|
233
|
+
type: "boolean",
|
|
234
|
+
group: "cursor",
|
|
235
|
+
description: "Show 'Zoom' on images, 'Play' on videos, 'View' on links when hovered",
|
|
236
|
+
initialValue: false,
|
|
237
|
+
}),
|
|
211
238
|
],
|
|
212
239
|
});
|
package/styles/base.css
CHANGED
|
@@ -41,6 +41,32 @@ html {
|
|
|
41
41
|
scroll-behavior: smooth;
|
|
42
42
|
overflow-x: clip;
|
|
43
43
|
scrollbar-gutter: stable;
|
|
44
|
+
/* Firefox */
|
|
45
|
+
scrollbar-width: thin;
|
|
46
|
+
scrollbar-color: rgba(128, 128, 128, 0.4) transparent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Custom scrollbar — public site (admin overrides via [data-admin]).
|
|
50
|
+
Thin, rounded thumb on a transparent track; works on both light and dark
|
|
51
|
+
backgrounds because the thumb is a semi-transparent neutral grey. */
|
|
52
|
+
::-webkit-scrollbar {
|
|
53
|
+
width: 10px;
|
|
54
|
+
height: 10px;
|
|
55
|
+
}
|
|
56
|
+
::-webkit-scrollbar-track {
|
|
57
|
+
background: transparent;
|
|
58
|
+
}
|
|
59
|
+
::-webkit-scrollbar-thumb {
|
|
60
|
+
background-color: rgba(128, 128, 128, 0.4);
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
border: 2px solid transparent;
|
|
63
|
+
background-clip: padding-box;
|
|
64
|
+
}
|
|
65
|
+
::-webkit-scrollbar-thumb:hover {
|
|
66
|
+
background-color: rgba(128, 128, 128, 0.7);
|
|
67
|
+
}
|
|
68
|
+
::-webkit-scrollbar-corner {
|
|
69
|
+
background: transparent;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
body {
|