Skip to content

Commit 8a780f7

Browse files
Mobile compatibility pass (#460)
* Clean up mobile iteration experience * Move edit button to action stack * Move actions out of toolbar and into global abstraction This will be useful for exposing charm actions shortly * fixing the variant thumbnail sizing * making variant multi generation on by default --------- Co-authored-by: jakedahn <jake@common.tools>
1 parent 987e94e commit 8a780f7

File tree

9 files changed

+1086
-499
lines changed

9 files changed

+1086
-499
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { animated } from "@react-spring/web";
2+
import { useActionManager } from "../contexts/ActionManagerContext";
3+
import { NavLink } from "react-router-dom";
4+
5+
export function ActionBar() {
6+
const { availableActions } = useActionManager();
7+
8+
return (
9+
<div className="fixed bottom-2 right-2 z-50 flex flex-row gap-2">
10+
{availableActions.map((action) => {
11+
// For NavLink actions
12+
if (action.id.startsWith("link:") && action.to) {
13+
return (
14+
<NavLink
15+
key={action.id}
16+
to={action.to}
17+
className={`
18+
flex items-center justify-center w-12 h-12
19+
border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)]
20+
hover:translate-y-[-2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,0.7)]
21+
transition-[border,box-shadow,transform] duration-100 ease-in-out
22+
bg-white cursor-pointer relative group
23+
touch-action-manipulation tap-highlight-color-transparent
24+
`}
25+
style={{
26+
touchAction: "manipulation",
27+
WebkitTapHighlightColor: "transparent",
28+
}}
29+
>
30+
{action.icon}
31+
<div className="absolute top-[-40px] left-1/2 -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
32+
{action.label}
33+
</div>
34+
</NavLink>
35+
);
36+
}
37+
38+
// For regular button actions
39+
return (
40+
<animated.button
41+
key={action.id}
42+
className={`
43+
flex items-center justify-center w-12 h-12
44+
border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)]
45+
hover:translate-y-[-2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,0.7)]
46+
transition-[border,box-shadow,transform] duration-100 ease-in-out
47+
bg-white cursor-pointer relative group
48+
touch-action-manipulation tap-highlight-color-transparent
49+
`}
50+
style={{
51+
touchAction: "manipulation",
52+
WebkitTapHighlightColor: "transparent",
53+
}}
54+
onClick={action.onClick}
55+
>
56+
{action.icon}
57+
<div className="absolute top-[-40px] left-1/2 -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
58+
{action.label}
59+
</div>
60+
</animated.button>
61+
);
62+
})}
63+
</div>
64+
);
65+
}

typescript/packages/jumble/src/components/CharmRunner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export function CharmRenderer({ charm, className = "" }: CharmRendererProps) {
153153
</div>
154154
</div>
155155
) : null}
156-
<div className={className} ref={containerRef}></div>
156+
<div className={className + " overflow-clip"} ref={containerRef}></div>
157157
</>
158158
);
159159
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// hooks/useCharmPublisher.tsx
2+
import { useState, useEffect } from "react";
3+
import { useNavigate } from "react-router-dom";
4+
import { useCharmManager } from "@/contexts/CharmManagerContext.tsx";
5+
import { TYPE } from "@commontools/builder";
6+
import { saveSpell } from "@/services/spellbook.ts";
7+
import { ShareDialog } from "@/components/spellbook/ShareDialog.tsx";
8+
9+
function useCharmPublisher() {
10+
const { charmManager } = useCharmManager();
11+
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
12+
const [isPublishing, setIsPublishing] = useState(false);
13+
const navigate = useNavigate();
14+
const [currentCharmId, setCurrentCharmId] = useState<string | undefined>();
15+
const [currentCharmName, setCurrentCharmName] = useState<string | null>(null);
16+
17+
useEffect(() => {
18+
const handlePublishCharm = (event: CustomEvent) => {
19+
const { charmId, charmName } = event.detail || {};
20+
if (charmId) {
21+
setCurrentCharmId(charmId);
22+
setCurrentCharmName(charmName || null);
23+
setIsShareDialogOpen(true);
24+
}
25+
};
26+
27+
window.addEventListener("publish-charm", handlePublishCharm as EventListener);
28+
29+
return () => {
30+
window.removeEventListener("publish-charm", handlePublishCharm as EventListener);
31+
};
32+
}, []);
33+
34+
const handleShare = async (data: { title: string; description: string; tags: string[] }) => {
35+
if (!currentCharmId) return;
36+
37+
setIsPublishing(true);
38+
try {
39+
const charm = await charmManager.get(currentCharmId);
40+
if (!charm) throw new Error("Charm not found");
41+
const spell = charm.getSourceCell()?.get();
42+
const spellId = spell?.[TYPE];
43+
if (!spellId) throw new Error("Spell not found");
44+
45+
const success = await saveSpell(spellId, spell, data.title, data.description, data.tags);
46+
47+
if (success) {
48+
const fullUrl = `${window.location.protocol}//${window.location.host}/spellbook/${spellId}`;
49+
try {
50+
await navigator.clipboard.writeText(fullUrl);
51+
} catch (err) {
52+
console.error("Failed to copy to clipboard:", err);
53+
}
54+
navigate(`/spellbook/${spellId}`);
55+
} else {
56+
throw new Error("Failed to publish");
57+
}
58+
} catch (error) {
59+
console.error("Failed to publish:", error);
60+
} finally {
61+
setIsPublishing(false);
62+
setIsShareDialogOpen(false);
63+
}
64+
};
65+
66+
return {
67+
isShareDialogOpen,
68+
setIsShareDialogOpen,
69+
isPublishing,
70+
handleShare,
71+
defaultTitle: currentCharmName || "",
72+
};
73+
}
74+
75+
export function CharmPublisher() {
76+
const { isShareDialogOpen, setIsShareDialogOpen, isPublishing, handleShare, defaultTitle } =
77+
useCharmPublisher();
78+
79+
return (
80+
<ShareDialog
81+
isOpen={isShareDialogOpen}
82+
onClose={() => setIsShareDialogOpen(false)}
83+
onSubmit={handleShare}
84+
defaultTitle={defaultTitle}
85+
isPublishing={isPublishing}
86+
/>
87+
);
88+
}
89+
90+
// Usage example for a button that triggers the publish flow:
91+
//
92+
// import { LuShare2 } from "react-icons/lu";
93+
//
94+
// function PublishButton({ charmId, charmName }: { charmId?: string, charmName?: string }) {
95+
// if (!charmId) return null;
96+
//
97+
// const handleClick = () => {
98+
// window.dispatchEvent(new CustomEvent('publish-charm', {
99+
// detail: { charmId, charmName }
100+
// }));
101+
// };
102+
//
103+
// return (
104+
// <button
105+
// onClick={handleClick}
106+
// className="w-10 h-10 flex items-center justify-center rounded-lg transition-colors relative group bg-transparent text-black hover:bg-gray-200 z-10 cursor-pointer"
107+
// >
108+
// <LuShare2 size={16} />
109+
// <div className="absolute top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
110+
// Publish
111+
// </div>
112+
// </button>
113+
// );
114+
// }

typescript/packages/jumble/src/components/ShellHeader.tsx

Lines changed: 2 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,19 @@
1-
import { useState, useEffect } from "react";
21
import { NavLink } from "react-router-dom";
3-
import { LuPencil, LuShare2 } from "react-icons/lu";
42
import ShapeLogo from "@/assets/ShapeLogo.svg";
53
import { NavPath } from "@/components/NavPath.tsx";
6-
import { ShareDialog } from "@/components/spellbook/ShareDialog.tsx";
74
import { useCharmManager } from "@/contexts/CharmManagerContext.tsx";
8-
import { NAME, TYPE } from "@commontools/builder";
9-
import { useNavigate } from "react-router-dom";
10-
import { saveSpell } from "@/services/spellbook.ts";
115
import { User } from "@/components/User.tsx";
126
import { useSyncedStatus } from "@/hooks/use-synced-status";
137

148
type ShellHeaderProps = {
159
replicaName?: string;
1610
charmId?: string;
17-
isDetailActive: boolean;
18-
togglePath: string;
1911
};
2012

21-
export function ShellHeader({
22-
replicaName,
23-
charmId,
24-
isDetailActive,
25-
togglePath,
26-
}: ShellHeaderProps) {
13+
export function ShellHeader({ replicaName, charmId }: ShellHeaderProps) {
2714
const { charmManager } = useCharmManager();
28-
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
29-
const [charmName, setCharmName] = useState<string | null>(null);
30-
const [isPublishing, setIsPublishing] = useState(false);
3115
const { isSyncing, lastSyncTime } = useSyncedStatus(charmManager);
32-
const navigate = useNavigate();
33-
useEffect(() => {
34-
let mounted = true;
35-
let cancel: (() => void) | undefined;
3616

37-
async function getCharm() {
38-
if (charmId) {
39-
const charm = await charmManager.get(charmId);
40-
cancel = charm?.key(NAME).sink((value) => {
41-
if (mounted) setCharmName(value ?? null);
42-
});
43-
}
44-
}
45-
getCharm();
46-
47-
return () => {
48-
mounted = false;
49-
cancel?.();
50-
};
51-
}, [charmId, charmManager]);
52-
53-
const handleShare = async (data: { title: string; description: string; tags: string[] }) => {
54-
if (!charmId) return;
55-
56-
setIsPublishing(true);
57-
try {
58-
const charm = await charmManager.get(charmId);
59-
if (!charm) throw new Error("Charm not found");
60-
const spell = charm.getSourceCell()?.get();
61-
const spellId = spell?.[TYPE];
62-
if (!spellId) throw new Error("Spell not found");
63-
64-
const success = await saveSpell(spellId, spell, data.title, data.description, data.tags);
65-
66-
if (success) {
67-
const fullUrl = `${window.location.protocol}//${window.location.host}/spellbook/${spellId}`;
68-
try {
69-
await navigator.clipboard.writeText(fullUrl);
70-
} catch (err) {
71-
console.error("Failed to copy to clipboard:", err);
72-
}
73-
navigate(`/spellbook/${spellId}`);
74-
} else {
75-
throw new Error("Failed to publish");
76-
}
77-
} catch (error) {
78-
console.error("Failed to publish:", error);
79-
} finally {
80-
setIsPublishing(false);
81-
setIsShareDialogOpen(false);
82-
}
83-
};
8417
return (
8518
<header className="flex bg-gray-50 items-center justify-between border-b-2 p-2">
8619
<div className="header-start flex items-center gap-2">
@@ -110,32 +43,7 @@ export function ShellHeader({
11043
</div>
11144
</div>
11245
<User />
113-
{charmId && (
114-
<>
115-
<NavLink
116-
to={togglePath}
117-
className={`w-10 h-10 flex items-center justify-center rounded-lg transition-colors relative group z-10 ${
118-
isDetailActive
119-
? "bg-gray-300 hover:bg-gray-400 text-black"
120-
: "bg-transparent text-black hover:bg-gray-200"
121-
}`}
122-
>
123-
<LuPencil size={16} />
124-
<div className="absolute top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
125-
Edit
126-
</div>
127-
</NavLink>
128-
<button
129-
onClick={() => setIsShareDialogOpen(true)}
130-
className="w-10 h-10 flex items-center justify-center rounded-lg transition-colors relative group bg-transparent text-black hover:bg-gray-200 z-10 cursor-pointer"
131-
>
132-
<LuShare2 size={16} />
133-
<div className="absolute top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
134-
Publish
135-
</div>
136-
</button>
137-
</>
138-
)}
46+
13947
<NavLink
14048
to="/spellbook"
14149
className="brand flex items-center gap-2 opacity-30 hover:opacity-100 transition-opacity duration-200 relative group cursor-pointer z-10"
@@ -146,14 +54,6 @@ export function ShellHeader({
14654
</div>
14755
</NavLink>
14856
</div>
149-
150-
<ShareDialog
151-
isOpen={isShareDialogOpen}
152-
onClose={() => setIsShareDialogOpen(false)}
153-
onSubmit={handleShare}
154-
defaultTitle={charmName || ""}
155-
isPublishing={isPublishing}
156-
/>
15757
</header>
15858
);
15959
}

typescript/packages/jumble/src/components/User.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function User() {
1111
if (!user) {
1212
return;
1313
}
14-
let did = user.verifier().did();
14+
const did = user.verifier().did();
1515
if (!ignore) {
1616
setDid(did);
1717
}
@@ -25,10 +25,10 @@ export function User() {
2525

2626
let h = "0";
2727
let s = "50%";
28-
let l = "50%";
28+
const l = "50%";
2929

3030
if (did) {
31-
let index = did.length - 4;
31+
const index = did.length - 4;
3232
// DID string is `did:key:z{REST}`. Taking the last 3 characters,
3333
// we use the first two added for hue.
3434
h = `${did.charCodeAt(index) + did.charCodeAt(index + 1)}`;

0 commit comments

Comments
 (0)