Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
10c7988e
Commit
10c7988e
authored
Jul 15, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: simplify version preview and release workflow
parent
2d6e5007
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
180 additions
and
82 deletions
+180
-82
build-manager.ts
backend/src/build/build-manager.ts
+14
-8
git-manager.ts
backend/src/git/git-manager.ts
+6
-0
schemas.ts
backend/src/schemas.ts
+1
-1
server.ts
backend/src/server.ts
+6
-4
create-site.ts
backend/src/sites/create-site.ts
+2
-1
site-agent-service.ts
backend/src/sites/site-agent-service.ts
+8
-2
site-repository.ts
backend/src/sites/site-repository.ts
+2
-1
site-version-service.ts
backend/src/sites/site-version-service.ts
+63
-20
App.tsx
frontend/src/App.tsx
+65
-42
api.ts
frontend/src/api.ts
+3
-2
styles.css
frontend/src/styles.css
+3
-1
index.ts
shared/src/index.ts
+7
-0
No files found.
backend/src/build/build-manager.ts
View file @
10c7988e
...
@@ -35,11 +35,24 @@ export class BuildManager {
...
@@ -35,11 +35,24 @@ export class BuildManager {
}
}
async
publishPreview
(
siteId
:
string
,
distPath
:
string
):
Promise
<
string
>
{
async
publishPreview
(
siteId
:
string
,
distPath
:
string
):
Promise
<
string
>
{
return
this
.
publish
To
(
path
.
join
(
runtimePaths
.
previews
,
siteId
),
distPath
);
return
this
.
publish
Atomically
(
path
.
join
(
runtimePaths
.
previews
,
siteId
),
distPath
);
}
}
async
publishProduction
(
siteId
:
string
,
distPath
:
string
):
Promise
<
string
>
{
async
publishProduction
(
siteId
:
string
,
distPath
:
string
):
Promise
<
string
>
{
return
this
.
publishAtomically
(
path
.
join
(
config
.
productionDir
,
siteId
),
distPath
);
}
async
ensureProductionPlaceholder
(
siteId
:
string
,
siteName
:
string
):
Promise
<
string
>
{
const
target
=
path
.
join
(
config
.
productionDir
,
siteId
);
const
target
=
path
.
join
(
config
.
productionDir
,
siteId
);
const
indexPath
=
path
.
join
(
target
,
"index.html"
);
if
(
await
stat
(
indexPath
).
then
((
value
)
=>
value
.
isFile
()).
catch
(()
=>
false
))
return
target
;
const
safeName
=
siteName
.
replaceAll
(
"&"
,
"&"
).
replaceAll
(
"<"
,
"<"
).
replaceAll
(
">"
,
">"
);
await
mkdir
(
target
,
{
recursive
:
true
});
await
writeFile
(
indexPath
,
`<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><title>
${
safeName
}
|尚未上线</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;color:#24242f;background:#f7f7fb;font-family:system-ui,"PingFang SC",sans-serif}.card{max-width:420px;padding:42px;text-align:center;background:white;border:1px solid #e8e5f1;border-radius:24px;box-shadow:0 22px 60px rgba(36,16,95,.09)}span{display:grid;place-items:center;width:48px;height:48px;margin:auto;color:white;background:linear-gradient(135deg,#7028ff,#536cff);border-radius:15px;font-weight:800}h1{margin:20px 0 10px;font-size:24px}p{margin:0;color:#777985;font-size:14px;line-height:1.8}</style></head><body><main class="card"><span>W</span><h1>
${
safeName
}
</h1><p>该官网尚未上线。<br>请先在测试环境确认效果后发布。</p></main></body></html>`
,
"utf8"
);
return
target
;
}
private
async
publishAtomically
(
target
:
string
,
distPath
:
string
):
Promise
<
string
>
{
const
suffix
=
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
suffix
=
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
staging
=
target
+
".next-"
+
suffix
;
const
staging
=
target
+
".next-"
+
suffix
;
const
previous
=
target
+
".previous-"
+
suffix
;
const
previous
=
target
+
".previous-"
+
suffix
;
...
@@ -57,11 +70,4 @@ export class BuildManager {
...
@@ -57,11 +70,4 @@ export class BuildManager {
await
rm
(
previous
,
{
recursive
:
true
,
force
:
true
});
await
rm
(
previous
,
{
recursive
:
true
,
force
:
true
});
return
target
;
return
target
;
}
}
private
async
publishTo
(
target
:
string
,
distPath
:
string
):
Promise
<
string
>
{
await
rm
(
target
,
{
recursive
:
true
,
force
:
true
});
await
mkdir
(
path
.
dirname
(
target
),
{
recursive
:
true
});
await
cp
(
distPath
,
target
,
{
recursive
:
true
});
return
target
;
}
}
}
backend/src/git/git-manager.ts
View file @
10c7988e
...
@@ -24,6 +24,12 @@ export class GitManager {
...
@@ -24,6 +24,12 @@ export class GitManager {
return
result
.
stdout
.
trim
();
return
result
.
stdout
.
trim
();
}
}
async
previousCommit
(
projectPath
:
string
,
commit
=
"HEAD"
):
Promise
<
string
|
undefined
>
{
await
this
.
assertCommit
(
projectPath
,
commit
);
const
result
=
await
execa
(
"git"
,
[
"rev-list"
,
"--parents"
,
"-n"
,
"1"
,
commit
],
{
cwd
:
projectPath
});
return
result
.
stdout
.
trim
().
split
(
/
\s
+/
)[
1
];
}
async
history
(
projectPath
:
string
):
Promise
<
GitHistoryItem
[]
>
{
async
history
(
projectPath
:
string
):
Promise
<
GitHistoryItem
[]
>
{
const
current
=
await
this
.
currentCommit
(
projectPath
);
const
current
=
await
this
.
currentCommit
(
projectPath
);
const
result
=
await
execa
(
"git"
,
[
"log"
,
"--date=iso-strict"
,
"--format=%H%x1f%h%x1f%s%x1f%an%x1f%cI%x1e"
,
"-30"
],
{
cwd
:
projectPath
});
const
result
=
await
execa
(
"git"
,
[
"log"
,
"--date=iso-strict"
,
"--format=%H%x1f%h%x1f%s%x1f%an%x1f%cI%x1e"
,
"-30"
],
{
cwd
:
projectPath
});
...
...
backend/src/schemas.ts
View file @
10c7988e
...
@@ -14,7 +14,7 @@ export const chatSchema = z.object({
...
@@ -14,7 +14,7 @@ export const chatSchema = z.object({
message
:
z
.
string
().
trim
().
min
(
2
).
max
(
2000
),
message
:
z
.
string
().
trim
().
min
(
2
).
max
(
2000
),
});
});
export
const
rollback
Schema
=
z
.
object
({
export
const
versionCommit
Schema
=
z
.
object
({
commit
:
z
.
string
().
regex
(
/^
[
0-9a-f
]{7,40}
$/
),
commit
:
z
.
string
().
regex
(
/^
[
0-9a-f
]{7,40}
$/
),
});
});
...
...
backend/src/server.ts
View file @
10c7988e
...
@@ -8,7 +8,7 @@ import { BuildManager } from "./build/build-manager.js";
...
@@ -8,7 +8,7 @@ import { BuildManager } from "./build/build-manager.js";
import
{
config
,
runtimePaths
}
from
"./config.js"
;
import
{
config
,
runtimePaths
}
from
"./config.js"
;
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
chatSchema
,
createSiteSchema
,
rollback
Schema
}
from
"./schemas.js"
;
import
{
chatSchema
,
createSiteSchema
,
versionCommit
Schema
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteAgentService
}
from
"./sites/site-agent-service.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
import
{
SiteRepository
}
from
"./sites/site-repository.js"
;
...
@@ -33,11 +33,12 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/chat", async (reque
...
@@ -33,11 +33,12 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/chat", async (reque
const
body
=
chatSchema
.
parse
(
request
.
body
);
return
siteAgent
.
execute
(
request
.
params
.
siteId
,
body
.
message
);
const
body
=
chatSchema
.
parse
(
request
.
body
);
return
siteAgent
.
execute
(
request
.
params
.
siteId
,
body
.
message
);
});
});
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/build"
,
async
(
request
)
=>
versions
.
rebuild
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/build"
,
async
(
request
)
=>
versions
.
rebuild
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/preview-version"
,
async
(
request
)
=>
{
const
body
=
versionCommitSchema
.
parse
(
request
.
body
);
return
versions
.
previewVersion
(
request
.
params
.
siteId
,
body
.
commit
);
});
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/publish"
,
async
(
request
)
=>
versions
.
publish
(
request
.
params
.
siteId
));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/publish"
,
async
(
request
)
=>
versions
.
publish
(
request
.
params
.
siteId
));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/history"
,
async
(
request
)
=>
git
.
history
(
sites
.
getProjectPath
(
request
.
params
.
siteId
)));
app
.
get
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/history"
,
async
(
request
)
=>
git
.
history
(
sites
.
getProjectPath
(
request
.
params
.
siteId
)));
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/rollback"
,
async
(
request
)
=>
{
app
.
post
<
{
Params
:
{
siteId
:
string
}
}
>
(
"/api/sites/:siteId/undo"
,
async
(
request
)
=>
versions
.
undo
(
request
.
params
.
siteId
));
const
body
=
rollbackSchema
.
parse
(
request
.
body
);
return
versions
.
rollback
(
request
.
params
.
siteId
,
body
.
commit
);
});
app
.
setErrorHandler
((
error
,
_request
,
reply
)
=>
{
app
.
setErrorHandler
((
error
,
_request
,
reply
)
=>
{
const
isMissingFile
=
error
instanceof
Error
&&
"code"
in
error
&&
error
.
code
===
"ENOENT"
;
const
isMissingFile
=
error
instanceof
Error
&&
"code"
in
error
&&
error
.
code
===
"ENOENT"
;
...
@@ -54,6 +55,7 @@ await rm(runtimePaths.builds, { recursive: true, force: true });
...
@@ -54,6 +55,7 @@ await rm(runtimePaths.builds, { recursive: true, force: true });
await
mkdir
(
runtimePaths
.
builds
,
{
recursive
:
true
});
await
mkdir
(
runtimePaths
.
builds
,
{
recursive
:
true
});
for
(
const
site
of
await
sites
.
list
())
{
for
(
const
site
of
await
sites
.
list
())
{
await
git
.
pruneWorktrees
(
sites
.
getProjectPath
(
site
.
siteId
));
await
git
.
pruneWorktrees
(
sites
.
getProjectPath
(
site
.
siteId
));
await
builds
.
ensureProductionPlaceholder
(
site
.
siteId
,
site
.
name
);
if
(
site
.
environmentVersion
!==
3
)
{
if
(
site
.
environmentVersion
!==
3
)
{
await
versions
.
rebuild
(
site
.
siteId
).
catch
((
error
)
=>
app
.
log
.
warn
(
error
));
await
versions
.
rebuild
(
site
.
siteId
).
catch
((
error
)
=>
app
.
log
.
warn
(
error
));
continue
;
continue
;
...
...
backend/src/sites/create-site.ts
View file @
10c7988e
...
@@ -48,7 +48,8 @@ export class CreateSiteService {
...
@@ -48,7 +48,8 @@ export class CreateSiteService {
await
this
.
sites
.
update
(
siteId
,
{
currentCommit
:
commit
});
await
this
.
sites
.
update
(
siteId
,
{
currentCommit
:
commit
});
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
previewPort
);
await
this
.
previews
.
start
(
siteId
,
published
,
previewPort
);
return
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
lastError
:
undefined
});
await
this
.
builds
.
ensureProductionPlaceholder
(
siteId
,
input
.
name
);
return
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
lastError
:
undefined
});
}
catch
(
error
)
{
}
catch
(
error
)
{
const
message
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
const
message
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"failed"
,
lastError
:
message
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
"failed"
,
lastError
:
message
});
...
...
backend/src/sites/site-agent-service.ts
View file @
10c7988e
...
@@ -19,6 +19,9 @@ export class SiteAgentService {
...
@@ -19,6 +19,9 @@ export class SiteAgentService {
async
execute
(
siteId
:
string
,
message
:
string
):
Promise
<
ChatResult
>
{
async
execute
(
siteId
:
string
,
message
:
string
):
Promise
<
ChatResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
site
=
await
this
.
sites
.
get
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前测试环境正在预览历史版本,请先切换到最近保存版本再继续修改"
);
}
const
projectPath
=
this
.
sites
.
getProjectPath
(
siteId
);
const
projectPath
=
this
.
sites
.
getProjectPath
(
siteId
);
const
taskId
=
"task_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
taskId
=
"task_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
...
@@ -49,11 +52,14 @@ export class SiteAgentService {
...
@@ -49,11 +52,14 @@ export class SiteAgentService {
const
commit
=
await
this
.
git
.
cherryPick
(
projectPath
,
worktreeCommit
);
const
commit
=
await
this
.
git
.
cherryPick
(
projectPath
,
worktreeCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
environmentVersion
:
3
,
lastError
:
undefined
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
previousCommit
:
site
.
currentCommit
,
environmentVersion
:
3
,
lastError
:
undefined
,
});
return
{
summary
:
generated
.
patch
.
summary
,
commit
,
previewUrl
:
site
.
previewUrl
,
changedFiles
,
mode
:
generated
.
mode
};
return
{
summary
:
generated
.
patch
.
summary
,
commit
,
previewUrl
:
site
.
previewUrl
,
changedFiles
,
mode
:
generated
.
mode
};
}
catch
(
error
)
{
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"failed"
,
lastError
:
details
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
throw
error
;
throw
error
;
}
finally
{
}
finally
{
await
this
.
git
.
removeWorktree
(
projectPath
,
workspace
);
await
this
.
git
.
removeWorktree
(
projectPath
,
workspace
);
...
...
backend/src/sites/site-repository.ts
View file @
10c7988e
...
@@ -30,7 +30,8 @@ export class SiteRepository {
...
@@ -30,7 +30,8 @@ export class SiteRepository {
return
{
return
{
...
stored
,
...
stored
,
previewUrl
:
getPublicPreviewUrl
(
siteId
),
previewUrl
:
getPublicPreviewUrl
(
siteId
),
productionUrl
:
stored
.
publishedCommit
?
getPublicProductionUrl
(
siteId
)
:
undefined
,
previewCommit
:
stored
.
previewCommit
||
stored
.
currentCommit
,
productionUrl
:
getPublicProductionUrl
(
siteId
),
publishStatus
:
stored
.
publishStatus
||
(
stored
.
publishedCommit
?
"published"
:
"unpublished"
),
publishStatus
:
stored
.
publishStatus
||
(
stored
.
publishedCommit
?
"published"
:
"unpublished"
),
};
};
}
}
...
...
backend/src/sites/site-version-service.ts
View file @
10c7988e
import
crypto
from
"node:crypto"
;
import
crypto
from
"node:crypto"
;
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
type
{
P
ublishResult
}
from
"@webagent/shared"
;
import
type
{
P
reviewVersionResult
,
PublishResult
,
SiteInfo
}
from
"@webagent/shared"
;
import
{
getPublicPreviewUrl
,
getPublicProductionUrl
,
runtimePaths
}
from
"../config.js"
;
import
{
getPublicPreviewUrl
,
getPublicProductionUrl
,
runtimePaths
}
from
"../config.js"
;
import
{
BuildError
,
BuildManager
}
from
"../build/build-manager.js"
;
import
{
BuildError
,
BuildManager
}
from
"../build/build-manager.js"
;
import
{
GitManager
}
from
"../git/git-manager.js"
;
import
{
GitManager
}
from
"../git/git-manager.js"
;
...
@@ -15,42 +15,53 @@ export class SiteVersionService {
...
@@ -15,42 +15,53 @@ export class SiteVersionService {
)
{}
)
{}
async
rebuild
(
siteId
:
string
):
Promise
<
{
previewUrl
:
string
}
>
{
async
rebuild
(
siteId
:
string
):
Promise
<
{
previewUrl
:
string
}
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
site
=
await
this
.
sites
.
get
(
siteId
);
let
currentCommit
=
await
this
.
git
.
currentCommit
(
project
);
let
previewCommit
=
site
.
previewCommit
||
site
.
currentCommit
||
currentCommit
;
if
(
await
ensureEnvironmentConfig
(
project
))
{
if
(
await
ensureEnvironmentConfig
(
project
))
{
await
this
.
git
.
commit
(
project
,
"System: support preview and production paths"
);
currentCommit
=
await
this
.
git
.
commit
(
project
,
"System: support preview and production paths"
);
}
if
(
previewCommit
===
site
.
currentCommit
)
previewCommit
=
currentCommit
;
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
try
{
const
dist
=
await
this
.
builds
.
build
(
project
,
"rebuild_"
+
siteId
,
getPublicPreviewUrl
(
siteId
));
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
await
this
.
git
.
currentCommit
(
project
),
environmentVersion
:
3
});
return
{
previewUrl
:
site
.
previewUrl
};
}
catch
(
error
)
{
await
this
.
sites
.
update
(
siteId
,
{
status
:
"failed"
,
lastError
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
});
throw
error
;
}
}
await
this
.
buildPreview
(
site
,
previewCommit
,
"rebuild"
,
{
currentCommit
});
return
{
previewUrl
:
site
.
previewUrl
};
}
}
async
rollback
(
siteId
:
string
,
targetCommit
:
string
):
Promise
<
{
commit
:
string
;
previewUrl
:
string
}
>
{
async
previewVersion
(
siteId
:
string
,
targetCommit
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
await
this
.
git
.
assertCommit
(
project
,
targetCommit
);
await
this
.
git
.
assertCommit
(
project
,
targetCommit
);
const
taskId
=
"rollback_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
await
this
.
buildPreview
(
site
,
targetCommit
,
"preview_version"
);
return
{
commit
:
targetCommit
,
previewUrl
:
site
.
previewUrl
};
}
async
undo
(
siteId
:
string
):
Promise
<
PreviewVersionResult
>
{
const
site
=
await
this
.
sites
.
get
(
siteId
);
const
project
=
this
.
sites
.
getProjectPath
(
siteId
);
if
(
site
.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
)
{
throw
new
Error
(
"当前正在查看历史版本,请先切换回最近保存版本再撤销修改"
);
}
const
currentCommit
=
await
this
.
git
.
currentCommit
(
project
);
const
targetCommit
=
site
.
previousCommit
||
await
this
.
git
.
previousCommit
(
project
,
currentCommit
);
if
(
!
targetCommit
)
throw
new
Error
(
"当前没有可撤销的上一版本"
);
await
this
.
git
.
assertCommit
(
project
,
targetCommit
);
const
taskId
=
"undo_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
try
{
try
{
await
this
.
git
.
createWorktree
(
project
,
workspace
,
targetCommit
);
await
this
.
git
.
createWorktree
(
project
,
workspace
,
targetCommit
);
await
ensureEnvironmentConfig
(
workspace
);
await
ensureEnvironmentConfig
(
workspace
);
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
siteId
));
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
const
commit
=
await
this
.
git
.
restoreAsCommit
(
project
,
targetCommit
);
const
commit
=
await
this
.
git
.
restoreAsCommit
(
project
,
targetCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
siteId
,
dist
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
previews
.
start
(
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
environmentVersion
:
3
,
lastError
:
undefined
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
previousCommit
:
undefined
,
environmentVersion
:
3
,
lastError
:
undefined
,
});
return
{
commit
,
previewUrl
:
site
.
previewUrl
};
return
{
commit
,
previewUrl
:
site
.
previewUrl
};
}
catch
(
error
)
{
}
catch
(
error
)
{
await
this
.
sites
.
update
(
siteId
,
{
status
:
"failed"
,
lastError
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
});
await
this
.
sites
.
update
(
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
error
instanceof
Error
?
error
.
message
:
String
(
error
)
});
throw
error
;
throw
error
;
}
finally
{
await
this
.
git
.
removeWorktree
(
project
,
workspace
);
}
}
finally
{
await
this
.
git
.
removeWorktree
(
project
,
workspace
);
}
}
}
...
@@ -63,8 +74,10 @@ export class SiteVersionService {
...
@@ -63,8 +74,10 @@ export class SiteVersionService {
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
await
this
.
sites
.
update
(
siteId
,
{
publishStatus
:
"publishing"
,
lastPublishError
:
undefined
});
await
this
.
sites
.
update
(
siteId
,
{
publishStatus
:
"publishing"
,
lastPublishError
:
undefined
});
try
{
try
{
const
commit
=
await
this
.
git
.
currentCommit
(
project
);
const
commit
=
site
.
previewCommit
||
site
.
currentCommit
;
await
this
.
git
.
assertCommit
(
project
,
commit
);
await
this
.
git
.
createWorktree
(
project
,
workspace
,
commit
);
await
this
.
git
.
createWorktree
(
project
,
workspace
,
commit
);
await
ensureEnvironmentConfig
(
workspace
);
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicProductionUrl
(
siteId
));
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicProductionUrl
(
siteId
));
await
this
.
builds
.
publishProduction
(
siteId
,
dist
);
await
this
.
builds
.
publishProduction
(
siteId
,
dist
);
const
publishedAt
=
new
Date
().
toISOString
();
const
publishedAt
=
new
Date
().
toISOString
();
...
@@ -82,4 +95,34 @@ export class SiteVersionService {
...
@@ -82,4 +95,34 @@ export class SiteVersionService {
await
this
.
git
.
removeWorktree
(
project
,
workspace
);
await
this
.
git
.
removeWorktree
(
project
,
workspace
);
}
}
}
}
private
async
buildPreview
(
site
:
SiteInfo
,
commit
:
string
,
taskPrefix
:
string
,
metadata
:
Partial
<
SiteInfo
>
=
{},
):
Promise
<
void
>
{
const
project
=
this
.
sites
.
getProjectPath
(
site
.
siteId
);
await
this
.
git
.
assertCommit
(
project
,
commit
);
const
taskId
=
taskPrefix
+
"_"
+
crypto
.
randomBytes
(
4
).
toString
(
"hex"
);
const
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
await
this
.
sites
.
update
(
site
.
siteId
,
{
status
:
"building"
,
lastError
:
undefined
});
try
{
await
this
.
git
.
createWorktree
(
project
,
workspace
,
commit
);
await
ensureEnvironmentConfig
(
workspace
);
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
getPublicPreviewUrl
(
site
.
siteId
));
const
published
=
await
this
.
builds
.
publishPreview
(
site
.
siteId
,
dist
);
await
this
.
previews
.
start
(
site
.
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
update
(
site
.
siteId
,
{
...
metadata
,
status
:
"ready"
,
previewCommit
:
commit
,
environmentVersion
:
3
,
lastError
:
undefined
,
});
}
catch
(
error
)
{
const
details
=
error
instanceof
BuildError
?
error
.
output
.
slice
(
-
3000
)
:
error
instanceof
Error
?
error
.
message
:
String
(
error
);
await
this
.
sites
.
update
(
site
.
siteId
,
{
status
:
site
.
previewCommit
?
"ready"
:
"failed"
,
lastError
:
details
});
throw
error
;
}
finally
{
await
this
.
git
.
removeWorktree
(
project
,
workspace
);
}
}
}
}
frontend/src/App.tsx
View file @
10c7988e
...
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
...
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import
{
import
{
ArrowLeft
,
ArrowRight
,
Bot
,
Check
,
ChevronDown
,
Clock3
,
Code2
,
ExternalLink
,
ArrowLeft
,
ArrowRight
,
Bot
,
Check
,
ChevronDown
,
Clock3
,
Code2
,
ExternalLink
,
Globe2
,
History
,
Laptop
,
LoaderCircle
,
MessageSquareText
,
Monitor
,
Palette
,
Globe2
,
History
,
Laptop
,
LoaderCircle
,
MessageSquareText
,
Monitor
,
Palette
,
Plus
,
RefreshCw
,
Rocket
,
RotateCcw
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Plus
,
RefreshCw
,
Rocket
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Undo2
,
WandSparkles
,
X
,
WandSparkles
,
X
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
type
{
CreateSiteInput
,
SiteInfo
}
from
"@webagent/shared"
;
import
type
{
CreateSiteInput
,
SiteInfo
}
from
"@webagent/shared"
;
...
@@ -156,6 +156,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -156,6 +156,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
const
[
tab
,
setTab
]
=
useState
<
"chat"
|
"history"
>
(
"chat"
);
const
[
tab
,
setTab
]
=
useState
<
"chat"
|
"history"
>
(
"chat"
);
const
[
device
,
setDevice
]
=
useState
<
"desktop"
|
"mobile"
>
(
"desktop"
);
const
[
device
,
setDevice
]
=
useState
<
"desktop"
|
"mobile"
>
(
"desktop"
);
const
[
environment
,
setEnvironment
]
=
useState
<
"preview"
|
"production"
>
(
"preview"
);
const
[
environment
,
setEnvironment
]
=
useState
<
"preview"
|
"production"
>
(
"preview"
);
const
[
pendingPreviewCommit
,
setPendingPreviewCommit
]
=
useState
(
""
);
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
input
,
setInput
]
=
useState
(
""
);
const
[
iframeVersion
,
setIframeVersion
]
=
useState
(
0
);
const
[
iframeVersion
,
setIframeVersion
]
=
useState
(
0
);
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([
...
@@ -163,9 +164,11 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -163,9 +164,11 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
]);
]);
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
site
=
siteQuery
.
data
;
const
site
=
siteQuery
.
data
;
const
historicalPreview
=
Boolean
(
site
?.
previewCommit
&&
site
.
previewCommit
!==
site
.
currentCommit
);
useEffect
(()
=>
{
scrollRef
.
current
?.
scrollTo
({
top
:
scrollRef
.
current
.
scrollHeight
,
behavior
:
"smooth"
});
},
[
messages
]);
useEffect
(()
=>
{
scrollRef
.
current
?.
scrollTo
({
top
:
scrollRef
.
current
.
scrollHeight
,
behavior
:
"smooth"
});
},
[
messages
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
setEnvironment
(
"preview"
);
setEnvironment
(
"preview"
);
setPendingPreviewCommit
(
""
);
setMessages
([{
role
:
"agent"
,
text
:
"已切换到这个网站。你可以继续描述修改要求。"
,
meta
:
agentMode
===
"model"
?
"模型 Agent 已连接"
:
"本地演示 Agent"
}]);
setMessages
([{
role
:
"agent"
,
text
:
"已切换到这个网站。你可以继续描述修改要求。"
,
meta
:
agentMode
===
"model"
?
"模型 Agent 已连接"
:
"本地演示 Agent"
}]);
},
[
siteId
,
agentMode
]);
},
[
siteId
,
agentMode
]);
...
@@ -195,9 +198,25 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -195,9 +198,25 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
},
},
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"重新构建失败,原测试预览保持可用"
}]),
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"重新构建失败,原测试预览保持可用"
}]),
});
});
const
rollbackMutation
=
useMutation
({
const
previewVersionMutation
=
useMutation
({
mutationFn
:
(
commit
:
string
)
=>
api
.
rollback
(
siteId
,
commit
),
mutationFn
:
(
commit
:
string
)
=>
api
.
previewVersion
(
siteId
,
commit
),
onSuccess
:
async
(
result
)
=>
{
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
"已恢复所选版本,并创建新的回滚记录。"
,
meta
:
result
.
commit
.
slice
(
0
,
7
)
}]);
await
refreshData
();
},
onMutate
:
(
commit
)
=>
setPendingPreviewCommit
(
commit
),
onSuccess
:
async
(
result
)
=>
{
setPendingPreviewCommit
(
""
);
setEnvironment
(
"preview"
);
await
refreshData
();
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
"已切换测试预览版本。"
,
meta
:
"测试版本 · "
+
result
.
commit
.
slice
(
0
,
7
)
}]);
},
onError
:
(
error
)
=>
{
setPendingPreviewCommit
(
""
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"版本预览失败,原测试版本保持可用"
}]);
},
});
const
undoMutation
=
useMutation
({
mutationFn
:
()
=>
api
.
undo
(
siteId
),
onSuccess
:
async
(
result
)
=>
{
setEnvironment
(
"preview"
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
"已撤销最近一次修改。"
,
meta
:
"测试环境已恢复 · "
+
result
.
commit
.
slice
(
0
,
7
)
}]);
await
refreshData
();
},
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"撤销失败,现有测试和线上版本保持可用"
}]),
});
});
const
publishMutation
=
useMutation
({
const
publishMutation
=
useMutation
({
mutationFn
:
()
=>
api
.
publish
(
siteId
),
mutationFn
:
()
=>
api
.
publish
(
siteId
),
...
@@ -209,21 +228,27 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -209,21 +228,27 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"发布失败,原生产版本保持可用"
}]),
onError
:
(
error
)
=>
setMessages
((
items
)
=>
[...
items
,
{
role
:
"agent"
,
text
:
error
.
message
,
meta
:
"发布失败,原生产版本保持可用"
}]),
});
});
const
send
=
(
message
=
input
)
=>
{
const
send
=
(
message
=
input
)
=>
{
const
value
=
message
.
trim
();
if
(
!
value
||
chatMutation
.
isPending
)
return
;
const
value
=
message
.
trim
();
if
(
!
value
||
chatMutation
.
isPending
||
historicalPreview
)
return
;
setMessages
((
items
)
=>
[...
items
,
{
role
:
"user"
,
text
:
value
}]);
setInput
(
""
);
chatMutation
.
mutate
(
value
);
setMessages
((
items
)
=>
[...
items
,
{
role
:
"user"
,
text
:
value
}]);
setInput
(
""
);
chatMutation
.
mutate
(
value
);
};
};
const
busy
=
chatMutation
.
isPending
||
rebuildMutation
.
isPending
||
rollbackMutation
.
isPending
||
publishMutation
.
isPending
;
const
busy
=
chatMutation
.
isPending
||
rebuildMutation
.
isPending
||
previewVersionMutation
.
isPending
||
undoMutation
.
isPending
||
publishMutation
.
isPending
;
const
productionCurrent
=
Boolean
(
site
?.
publishedCommit
&&
site
.
publishedCommit
===
site
.
currentCommit
);
const
canPublish
=
site
?.
status
===
"ready"
&&
!
productionCurrent
;
if
(
!
site
)
return
<
Splash
/>;
if
(
!
site
)
return
<
Splash
/>;
const
previewVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
currentCommit
);
const
previewVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
previewCommit
);
const
currentVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
currentCommit
);
const
productionVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
publishedCommit
);
const
productionVersion
=
historyQuery
.
data
?.
find
((
item
)
=>
item
.
hash
===
site
.
publishedCommit
);
const
productionMatchesPreview
=
Boolean
(
site
.
publishedCommit
&&
site
.
publishedCommit
===
site
.
previewCommit
);
const
canPublish
=
site
.
status
===
"ready"
&&
Boolean
(
site
.
previewCommit
)
&&
!
productionMatchesPreview
;
const
canUndo
=
!
historicalPreview
&&
Boolean
(
site
.
previousCommit
||
currentVersion
?.
message
.
startsWith
(
"Agent:"
));
const
showingProduction
=
environment
===
"production"
;
const
showingProduction
=
environment
===
"production"
;
const
activeUrl
=
showingProduction
?
site
.
productionUrl
:
site
.
previewUrl
;
const
activeUrl
=
showingProduction
?
site
.
productionUrl
:
site
.
previewUrl
;
const
environmentStatus
=
showingProduction
const
environmentStatus
=
showingProduction
?
!
site
.
productionUrl
?
"尚未上线"
:
productionCurrent
?
"生产已同步"
:
"生产版本较旧"
?
site
.
lastPublishError
?
"上线失败 · 线上未变"
:
!
site
.
publishedCommit
?
"尚未上线"
:
productionMatchesPreview
?
"生产已同步"
:
"生产版本较旧"
:
site
.
status
===
"ready"
?
"测试已同步"
:
"需要检查"
;
:
site
.
lastError
?
"构建失败 · 预览未变"
:
site
.
status
===
"ready"
?
"测试已同步"
:
"需要检查"
;
const
viewingLabel
=
showingProduction
?
site
.
publishedCommit
?
`线上版本
${
site
.
publishedCommit
.
slice
(
0
,
7
)}
`
:
"尚未上线"
:
historicalPreview
?
`历史版本
${
site
.
previewCommit
?.
slice
(
0
,
7
)}
` : `
测试版本
$
{
site
.
previewCommit
?.
slice
(
0
,
7
)
||
"-------"
}
`;
const savedState = chatMutation.isPending ? "正在修改" : site.lastError ? "构建失败" : "已保存";
return <main className="workspace">
return <main className="workspace">
<aside className="rail">
<aside className="rail">
<Logo compact />
<Logo compact />
...
@@ -243,42 +268,34 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -243,42 +268,34 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
{chatMutation.isPending && <div className="agent-progress"><span className="agent-avatar"><Sparkles size={14} /></span><div><p><LoaderCircle className="spin" size={14} /> Agent 正在设计和验证</p><div className="progress-track"><i /></div><small>读取文件 → 生成 Patch → 构建检查</small></div></div>}
{chatMutation.isPending && <div className="agent-progress"><span className="agent-avatar"><Sparkles size={14} /></span><div><p><LoaderCircle className="spin" size={14} /> Agent 正在设计和验证</p><div className="progress-track"><i /></div><small>读取文件 → 生成 Patch → 构建检查</small></div></div>}
{messages.length <= 1 && <div className="suggestions"><span>试试这些修改</span>{["把网站主色改成深蓝色", "增加三个服务卡片", "增加一个企业优势"].map((text) => <button key={text} onClick={() => send(text)}><Sparkles size={12} />{text}</button>)}</div>}
{messages.length <= 1 && <div className="suggestions"><span>试试这些修改</span>{["把网站主色改成深蓝色", "增加三个服务卡片", "增加一个企业优势"].map((text) => <button key={text} onClick={() => send(text)}><Sparkles size={12} />{text}</button>)}</div>}
</div>
</div>
{historicalPreview && <div className="history-edit-lock"><div><History size={14} /><span>当前是只读历史预览;切换到“最近保存”版本后才能继续修改</span></div></div>}
<div className="composer">
<div className="composer">
<
textarea
rows=
{
3
}
value=
{
input
}
disabled=
{
busy
}
placeholder=
"描述你想要的修改,例如:让首屏更有科技感…"
onChange=
{
(
e
)
=>
setInput
(
e
.
target
.
value
)
}
onKeyDown=
{
(
e
)
=>
{
if
(
e
.
key
===
"Enter"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
send
();
}
}
}
/>
<textarea rows={3} value={input} disabled={busy
|| historicalPreview} placeholder={historicalPreview ? "切换到最近保存版本后才能继续修改…" : "描述你想要的修改,例如:让首屏更有科技感…"}
onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} />
<
div
><
span
>
{
agentMode
===
"model"
?
<><
span
className=
"online-dot"
/>
模型 Agent
</>
:
<><
Code2
size=
{
12
}
/>
本地演示模式
</>
}
</
span
><
button
onClick=
{
()
=>
send
()
}
disabled=
{
!
input
.
trim
()
||
busy
}
><
Send
size=
{
15
}
/></
button
></
div
>
<div><span>{agentMode === "model" ? <><span className="online-dot" />模型 Agent</> : <><Code2 size={12} />本地演示模式</>}</span><button onClick={() => send()} disabled={!input.trim() || busy
|| historicalPreview
}><Send size={15} /></button></div>
</div>
</div>
</> : <div className="history-list">
</> : <div className="history-list">
<
div
className=
"environment-versions"
>
<div className="history-heading"><div><h3>测试版本</h3><p>点击右侧“预览”切换测试版本,生产环境不会改变</p></div><span>{historyQuery.data?.length || 0}</span></div>
<
button
className=
{
environment
===
"preview"
?
"active preview"
:
"preview"
}
onClick=
{
()
=>
setEnvironment
(
"preview"
)
}
>
{historicalPreview && <div className="history-edit-lock history-panel-lock"><div><History size={14} /><span>历史版本只能预览或上线;切换到“最近保存”版本后才能继续修改</span></div></div>}
<
span
><
Monitor
size=
{
14
}
/>
测试环境
<
em
>
当前预览
</
em
></
span
>
<
strong
>
{
site
.
currentCommit
.
slice
(
0
,
7
)
}
</
strong
>
<
small
>
{
previewVersion
?
formatDate
(
previewVersion
.
date
)
:
formatDate
(
site
.
updatedAt
)
}
</
small
>
</
button
>
<
button
className=
{
environment
===
"production"
?
"active production"
:
"production"
}
onClick=
{
()
=>
setEnvironment
(
"production"
)
}
>
<
span
><
Globe2
size=
{
14
}
/>
生产环境
<
em
>
{
site
.
productionUrl
?
productionCurrent
?
"已同步"
:
"待更新"
:
"未上线"
}
</
em
></
span
>
<
strong
>
{
site
.
publishedCommit
?.
slice
(
0
,
7
)
||
"-------"
}
</
strong
>
<
small
>
{
site
.
publishedAt
?
formatDate
(
site
.
publishedAt
)
:
"发布后生成生产链接"
}
</
small
>
</
button
>
</
div
>
<
div
className=
"history-heading"
><
div
><
h3
>
历史版本
</
h3
><
p
>
选择旧版本可恢复到测试环境
</
p
></
div
><
span
>
{
historyQuery
.
data
?.
length
||
0
}
</
span
></
div
>
{historyQuery.data?.map((item, index) => {
{historyQuery.data?.map((item, index) => {
const
isPreview
=
item
.
hash
===
site
.
current
Commit
;
const isPreview = item.hash === site.
preview
Commit;
const isProduction = item.hash === site.publishedCommit;
const isProduction = item.hash === site.publishedCommit;
return
<
article
className=
{
isPreview
||
isProduction
?
"environment-version"
:
""
}
key=
{
item
.
hash
}
>
const isCurrent = item.hash === site.currentCommit;
<
div
className=
"timeline"
><
i
>
{
item
.
current
?
<
Check
size=
{
10
}
/>
:
""
}
</
i
>
{
index
<
(
historyQuery
.
data
?.
length
||
0
)
-
1
&&
<
span
/>
}
</
div
>
const selecting = pendingPreviewCommit === item.hash;
<
div
className=
"commit-info"
><
div
><
strong
>
{
item
.
message
}
</
strong
>
{
isPreview
&&
<
em
className=
"preview-badge"
>
测试
</
em
>
}{
isProduction
&&
<
em
className=
"production-badge"
>
生产
</
em
>
}
</
div
><
p
><
code
>
{
item
.
shortHash
}
</
code
><
span
>
·
</
span
>
{
formatDate
(
item
.
date
)
}
</
p
></
div
>
return <article className={isPreview ? "selected" : ""} key={item.hash}>
<
div
className=
"history-actions"
>
<div className="history-row" aria-current={isPreview ? "page" : undefined}>
{
isPreview
&&
<
button
title=
"查看测试环境"
onClick=
{
()
=>
setEnvironment
(
"preview"
)
}
><
Monitor
size=
{
13
}
/></
button
>
}
<span className="timeline"><i>{selecting ? <LoaderCircle className="spin" size={10} /> : isPreview ? <Check size={10} /> : ""}</i>{index < (historyQuery.data?.length || 0) - 1 && <span />}</span>
{
isProduction
&&
<
button
title=
"查看生产环境"
onClick=
{
()
=>
setEnvironment
(
"production"
)
}
><
Globe2
size=
{
13
}
/></
button
>
}
<span className="commit-info"><span><strong>{item.message}</strong>{isPreview && <em className="preview-badge">当前查看</em>}{isProduction && <em className="production-badge">线上版本</em>}{isCurrent && <em className="current-badge">最近保存</em>}</span><small><code>{item.shortHash}</code><span>·</span>{formatDate(item.date)}</small></span>
{
!
isPreview
&&
<
button
title=
"恢复到测试环境"
disabled=
{
busy
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
"确定将测试环境恢复到版本 "
+
item
.
shortHash
+
" 吗?生产环境不会受到影响。"
))
rollbackMutation
.
mutate
(
item
.
hash
);
}
}
><
RotateCcw
size=
{
13
}
/></
button
>
}
{isPreview
</
div
>
? <span className="history-row-state">正在查看</span>
</
article
>;
: <button className="history-preview-button" type="button" disabled={busy} onClick={() => { setEnvironment("preview"); previewVersionMutation.mutate(item.hash); }}>{selecting ? <LoaderCircle className="spin" size={10} /> : <Monitor size={11} />}{selecting ? "切换中" : "预览"}</button>}
</div>
</article>;
})}
})}
</div>}
</div>}
</section>
</section>
<section className="preview-shell">
<section className="preview-shell">
<header className="preview-toolbar">
<header className="preview-toolbar">
<
div
className=
"preview-title"
><
div
><
span
className=
{
`status-dot ${showingProduction ? "production" : ""}`
}
/><
strong
>
{
site
.
name
}
</
strong
><
em
className=
{
showingProduction
?
"production"
:
""
}
>
{
showingProduction
?
"生产环境"
:
"测试环境"
}
</
em
></
div
><
span
className=
{
`build-status ${showingProduction ? site.
publishStatus : site.status}`
}
>
{
busy
?
<
LoaderCircle
className=
"spin"
size=
{
12
}
/>
:
<
Check
size=
{
12
}
/>
}{
publishMutation
.
isPending
?
"正在发布"
:
rebuild
Mutation
.
isPending
?
"正在构建"
:
environmentStatus
}
</
span
></
div
>
<div className="preview-title"><div><span className={`
status
-
dot
$
{
showingProduction
?
"production"
:
""
}
`} /><strong>{site.name}</strong><em className={showingProduction ? "production" : ""}>{showingProduction ? "生产环境" : "测试环境"}</em></div><span className={`
build
-
status
$
{
showingProduction
?
site
.
lastPublishError
?
"failed"
:
site
.
publishStatus
:
site
.
lastError
?
"failed"
:
site
.
status
}
`}>{busy ? <LoaderCircle className="spin" size={12} /> : showingProduction ? site.lastPublishError ? <X size={12} /> : <Check size={12} /> : site.lastError ? <X size={12} /> : <Check size={12} />}{publishMutation.isPending ? "正在发布" : chatMutation.isPending || rebuildMutation.isPending || previewVersionMutation.isPending || undo
Mutation.isPending ? "正在构建" : environmentStatus}</span></div>
<div className="preview-modes">
<div className="preview-modes">
<div className="environment-toggle"><button className={!showingProduction ? "active" : ""} onClick={() => setEnvironment("preview")}><Monitor size={13} />测试预览</button><button className={showingProduction ? "active" : ""} onClick={() => setEnvironment("production")}><Globe2 size={13} />生产站点</button></div>
<div className="environment-toggle"><button className={!showingProduction ? "active" : ""} onClick={() => setEnvironment("preview")}><Monitor size={13} />测试预览</button><button className={showingProduction ? "active" : ""} onClick={() => setEnvironment("production")}><Globe2 size={13} />生产站点</button></div>
<div className="device-toggle"><button className={device === "desktop" ? "active" : ""} onClick={() => setDevice("desktop")}><Laptop size={15} /></button><button className={device === "mobile" ? "active" : ""} onClick={() => setDevice("mobile")}><Smartphone size={15} /></button></div>
<div className="device-toggle"><button className={device === "desktop" ? "active" : ""} onClick={() => setDevice("desktop")}><Laptop size={15} /></button><button className={device === "mobile" ? "active" : ""} onClick={() => setDevice("mobile")}><Smartphone size={15} /></button></div>
...
@@ -286,16 +303,22 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
...
@@ -286,16 +303,22 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate }: { siteI
<div className="toolbar-actions">
<div className="toolbar-actions">
{!showingProduction && <button className="rebuild-button" disabled={busy} onClick={() => rebuildMutation.mutate()} title="重新构建测试环境"><RefreshCw className={rebuildMutation.isPending ? "spin" : ""} size={14} /><span>重新构建</span></button>}
{!showingProduction && <button className="rebuild-button" disabled={busy} onClick={() => rebuildMutation.mutate()} title="重新构建测试环境"><RefreshCw className={rebuildMutation.isPending ? "spin" : ""} size={14} /><span>重新构建</span></button>}
<a className="environment-link preview-link" href={site.previewUrl} target="_blank" rel="noreferrer" title="打开测试环境"><Monitor size={13} /><span>测试链接</span></a>
<a className="environment-link preview-link" href={site.previewUrl} target="_blank" rel="noreferrer" title="打开测试环境"><Monitor size={13} /><span>测试链接</span></a>
{
site
.
productionUrl
?
<
a
className=
"environment-link production-link"
href=
{
site
.
productionUrl
}
target=
"_blank"
rel=
"noreferrer"
title=
"打开生产环境"
><
Globe2
size=
{
13
}
/><
span
>
生产链接
</
span
></
a
>
:
<
button
className=
"environment-link production-link"
disabled
title=
"发布上线后生成生产链接"
><
Globe2
size=
{
13
}
/><
span
>
生产链接
</
span
></
button
>
}
<a className="environment-link production-link" href={site.productionUrl} target="_blank" rel="noreferrer" title="打开生产环境"><Globe2 size={13} /><span>生产链接</span></a>
<
button
className=
"publish-button"
disabled=
{
busy
||
!
canPublish
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
"确定将当前测试版本发布到生产环境吗?"
))
publishMutation
.
mutate
();
}
}
title=
{
productionCurrent
?
"当前版本已经上线"
:
site
.
status
!==
"ready"
?
"测试环境构建成功后才能发布"
:
"发布当前测试版本到生产环境"
}
>
{!showingProduction && <button className="undo-button" disabled={busy || !canUndo} onClick={() => { if (window.confirm("确定撤销最近一次修改吗?测试环境将恢复到上一份已保存版本,线上版本不会改变。")) undoMutation.mutate(); }} title={historicalPreview ? "请先切换回最近保存版本" : canUndo ? "恢复上一份已保存版本" : "当前没有可撤销的修改"}><Undo2 size={14} /><span>撤销最近修改</span></button>}
{
publishMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
14
}
/>
:
<
Rocket
size=
{
14
}
/>
}
<
span
>
{
productionCurrent
?
"已上线"
:
site
.
productionUrl
?
"发布更新"
:
"发布上线"
}
</
span
>
{!showingProduction && <button className="publish-button" disabled={busy || !canPublish} onClick={() => { if (window.confirm("确定将当前测试预览版本发布到生产环境吗?")) publishMutation.mutate(); }} title={productionMatchesPreview ? "当前测试版本已经上线" : site.status !== "ready" ? "测试环境构建成功后才能发布" : "发布当前测试预览版本到生产环境"}>
</
button
>
{publishMutation.isPending ? <LoaderCircle className="spin" size={14} /> : <Rocket size={14} />}<span>{productionMatchesPreview ? "已上线" : "上线当前版本"}</span>
</button>}
</div>
</div>
</header>
</header>
<section className="version-state-strip">
<article><span>当前正在看</span><strong>{viewingLabel}</strong><small>{showingProduction ? "生产链接" : historicalPreview ? "只读测试预览" : "测试链接"}</small></article>
<article className={site.lastError ? "error" : ""}><span>最近保存 <em>{savedState}</em></span><strong>{site.currentCommit.slice(0, 7)}</strong><small>{site.lastError ? "修改失败,已保留原测试版本" : "最近一次成功保存的可编辑版本"}</small></article>
<article className="production"><span>线上运行</span><strong>{site.publishedCommit?.slice(0, 7) || "尚未上线"}</strong><small>{site.lastPublishError ? "上线失败,原线上版本保持不变" : site.publishedAt ? formatDate(site.publishedAt) : "生产链接已保留"}</small></article>
</section>
<div className="browser-stage">
<div className="browser-stage">
<div className={`
browser
-
frame
$
{
device
}
`}>
<div className={`
browser
-
frame
$
{
device
}
`}>
<div className="browser-chrome"><div><i /><i /><i /></div><span>{activeUrl || site.productionUrl || "/sites/" + site.siteId + "/"}</span><RefreshCw size={11} /></div>
<div className="browser-chrome"><div><i /><i /><i /></div><span>{activeUrl || site.productionUrl || "/sites/" + site.siteId + "/"}</span><RefreshCw size={11} /></div>
{
activeUrl
?
<
iframe
key=
{
environment
+
iframeVersion
}
title=
{
site
.
name
+
(
showingProduction
?
" 生产站点"
:
" 测试预览"
)
}
src=
{
activeUrl
+
"?v="
+
iframeVersion
}
/>
:
<
div
className=
"production-empty"
><
span
><
Globe2
size=
{
24
}
/></
span
><
strong
>
生产环境尚未发布
</
strong
><
p
>
确认测试效果后,点击“发布上线”生成独立生产版本和链接。
</
p
><
button
disabled=
{
!
canPublish
||
busy
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
"确定将当前测试版本发布到生产环境吗?"
))
publishMutation
.
mutate
();
}
}
><
Rocket
size=
{
14
}
/>
发布当前测试版本
</
button
></
div
>
}
<iframe key={environment + iframeVersion} title={site.name + (showingProduction ? " 生产站点" : " 测试预览")} src={activeUrl + "?v=" + iframeVersion} />
{busy && <div className="preview-busy"><LoaderCircle className="spin" size={25} /><strong>{publishMutation.isPending ? "正在发布生产版本" : "正在生成测试版本"}</strong><span>{publishMutation.isPending ? "测试环境和原生产版本保持可用" : "当前测试预览保持可用"}</span></div>}
{busy && <div className="preview-busy"><LoaderCircle className="spin" size={25} /><strong>{publishMutation.isPending ? "正在发布生产版本" : "正在生成测试版本"}</strong><span>{publishMutation.isPending ? "测试环境和原生产版本保持可用" : "当前测试预览保持可用"}</span></div>}
</div>
</div>
</div>
</div>
...
...
frontend/src/api.ts
View file @
10c7988e
import
type
{
ChatResult
,
CreateSiteInput
,
GitHistoryItem
,
PublishResult
,
SiteInfo
}
from
"@webagent/shared"
;
import
type
{
ChatResult
,
CreateSiteInput
,
GitHistoryItem
,
P
reviewVersionResult
,
P
ublishResult
,
SiteInfo
}
from
"@webagent/shared"
;
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
async
function
request
<
T
>
(
url
:
string
,
options
?:
RequestInit
):
Promise
<
T
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
const
headers
=
new
Headers
(
options
?.
headers
);
...
@@ -19,7 +19,8 @@ export const api = {
...
@@ -19,7 +19,8 @@ export const api = {
createSite
:
(
input
:
CreateSiteInput
)
=>
request
<
SiteInfo
>
(
"/api/sites"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
)
}),
createSite
:
(
input
:
CreateSiteInput
)
=>
request
<
SiteInfo
>
(
"/api/sites"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
)
}),
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
chat
:
(
siteId
:
string
,
message
:
string
)
=>
request
<
ChatResult
>
(
"/api/sites/"
+
siteId
+
"/chat"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
message
})
}),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
history
:
(
siteId
:
string
)
=>
request
<
GitHistoryItem
[]
>
(
"/api/sites/"
+
siteId
+
"/history"
),
previewVersion
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/preview-version"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
method
:
"POST"
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
method
:
"POST"
}),
publish
:
(
siteId
:
string
)
=>
request
<
PublishResult
>
(
"/api/sites/"
+
siteId
+
"/publish"
,
{
method
:
"POST"
}),
publish
:
(
siteId
:
string
)
=>
request
<
PublishResult
>
(
"/api/sites/"
+
siteId
+
"/publish"
,
{
method
:
"POST"
}),
rollback
:
(
siteId
:
string
,
commit
:
string
)
=>
request
<
{
commit
:
string
;
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/rollback"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
commit
})
}),
undo
:
(
siteId
:
string
)
=>
request
<
PreviewVersionResult
>
(
"/api/sites/"
+
siteId
+
"/undo"
,
{
method
:
"POST"
}),
};
};
frontend/src/styles.css
View file @
10c7988e
...
@@ -18,7 +18,9 @@
...
@@ -18,7 +18,9 @@
.chat-scroll
{
flex
:
1
1
0
;
min-height
:
0
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
overscroll-behavior
:
contain
;
padding
:
20px
17px
28px
}
.chat-date
{
text-align
:
center
;
color
:
#aaaeba
;
font-size
:
9px
;
margin-bottom
:
22px
}
.message
{
display
:
flex
;
gap
:
9px
;
margin-bottom
:
17px
}
.message.user
{
justify-content
:
flex-end
}
.message
>
div
{
max-width
:
86%
}
.message
p
{
margin
:
0
;
padding
:
12px
14px
;
color
:
var
(
--text-2
);
background
:
#f6f6f9
;
border-radius
:
4px
13px
13px
13px
;
font-size
:
12px
;
line-height
:
1.65
}
.message.user
p
{
color
:
white
;
background
:
var
(
--gradient
);
border-radius
:
13px
4px
13px
13px
;
box-shadow
:
0
8px
18px
rgba
(
112
,
40
,
255
,
.15
)}
.message
small
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
margin
:
6px
2px
0
;
color
:
#9a9da8
;
font-size
:
9px
}
.agent-avatar
{
flex
:
0
0
auto
;
display
:
grid
;
place-items
:
center
;
width
:
28px
;
height
:
28px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border
:
1px
solid
#e2dbff
;
border-radius
:
9px
}
.agent-progress
{
display
:
flex
;
gap
:
9px
;
margin
:
8px
0
18px
}
.agent-progress
>
div
{
flex
:
1
;
padding
:
12px
13px
;
background
:
#faf9ff
;
border
:
1px
solid
#ebe6ff
;
border-radius
:
4px
13px
13px
13px
}
.agent-progress
p
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin
:
0
0
10px
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.agent-progress
small
{
color
:
var
(
--muted
);
font-size
:
9px
}
.progress-track
{
height
:
3px
;
margin-bottom
:
8px
;
overflow
:
hidden
;
background
:
#ebe7f5
;
border-radius
:
9px
}
.progress-track
i
{
display
:
block
;
width
:
45%
;
height
:
100%
;
background
:
var
(
--gradient
);
border-radius
:
9px
;
animation
:
progress
1.5s
ease-in-out
infinite
}
@keyframes
progress
{
50
%
{
transform
:
translateX
(
120%
)}}
.suggestions
{
display
:
flex
;
flex-direction
:
column
;
gap
:
7px
;
margin
:
25px
37px
0
}
.suggestions
>
span
{
margin-bottom
:
3px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.suggestions
button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
9px
11px
;
text-align
:
left
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
font-size
:
10px
}
.suggestions
button
:hover
{
color
:
var
(
--primary
);
border-color
:
#cdbdff
;
background
:
#faf9ff
}
.chat-scroll
{
flex
:
1
1
0
;
min-height
:
0
;
overflow-x
:
hidden
;
overflow-y
:
auto
;
overscroll-behavior
:
contain
;
padding
:
20px
17px
28px
}
.chat-date
{
text-align
:
center
;
color
:
#aaaeba
;
font-size
:
9px
;
margin-bottom
:
22px
}
.message
{
display
:
flex
;
gap
:
9px
;
margin-bottom
:
17px
}
.message.user
{
justify-content
:
flex-end
}
.message
>
div
{
max-width
:
86%
}
.message
p
{
margin
:
0
;
padding
:
12px
14px
;
color
:
var
(
--text-2
);
background
:
#f6f6f9
;
border-radius
:
4px
13px
13px
13px
;
font-size
:
12px
;
line-height
:
1.65
}
.message.user
p
{
color
:
white
;
background
:
var
(
--gradient
);
border-radius
:
13px
4px
13px
13px
;
box-shadow
:
0
8px
18px
rgba
(
112
,
40
,
255
,
.15
)}
.message
small
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
margin
:
6px
2px
0
;
color
:
#9a9da8
;
font-size
:
9px
}
.agent-avatar
{
flex
:
0
0
auto
;
display
:
grid
;
place-items
:
center
;
width
:
28px
;
height
:
28px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border
:
1px
solid
#e2dbff
;
border-radius
:
9px
}
.agent-progress
{
display
:
flex
;
gap
:
9px
;
margin
:
8px
0
18px
}
.agent-progress
>
div
{
flex
:
1
;
padding
:
12px
13px
;
background
:
#faf9ff
;
border
:
1px
solid
#ebe6ff
;
border-radius
:
4px
13px
13px
13px
}
.agent-progress
p
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
margin
:
0
0
10px
;
color
:
var
(
--text-2
);
font-size
:
11px
;
font-weight
:
700
}
.agent-progress
small
{
color
:
var
(
--muted
);
font-size
:
9px
}
.progress-track
{
height
:
3px
;
margin-bottom
:
8px
;
overflow
:
hidden
;
background
:
#ebe7f5
;
border-radius
:
9px
}
.progress-track
i
{
display
:
block
;
width
:
45%
;
height
:
100%
;
background
:
var
(
--gradient
);
border-radius
:
9px
;
animation
:
progress
1.5s
ease-in-out
infinite
}
@keyframes
progress
{
50
%
{
transform
:
translateX
(
120%
)}}
.suggestions
{
display
:
flex
;
flex-direction
:
column
;
gap
:
7px
;
margin
:
25px
37px
0
}
.suggestions
>
span
{
margin-bottom
:
3px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.suggestions
button
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
9px
11px
;
text-align
:
left
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
font-size
:
10px
}
.suggestions
button
:hover
{
color
:
var
(
--primary
);
border-color
:
#cdbdff
;
background
:
#faf9ff
}
.composer
{
flex
:
0
0
auto
;
margin
:
0
14px
14px
;
padding
:
10px
;
background
:
#fff
;
border
:
1px
solid
#dcd8e8
;
border-radius
:
13px
;
box-shadow
:
0
5px
22px
rgba
(
36
,
16
,
95
,
.07
)}
.composer
textarea
{
width
:
100%
;
padding
:
2px
4px
;
resize
:
none
;
color
:
var
(
--text
);
background
:
transparent
;
border
:
0
;
outline
:
none
;
font-size
:
11px
;
line-height
:
1.55
}
.composer
textarea
::placeholder
{
color
:
#a5a7b0
}
.composer
>
div
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.composer
>
div
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.online-dot
{
width
:
6px
;
height
:
6px
;
border-radius
:
50%
;
background
:
#20b486
}
.composer
button
{
display
:
grid
;
place-items
:
center
;
width
:
29px
;
height
:
29px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
8px
}
.composer
{
flex
:
0
0
auto
;
margin
:
0
14px
14px
;
padding
:
10px
;
background
:
#fff
;
border
:
1px
solid
#dcd8e8
;
border-radius
:
13px
;
box-shadow
:
0
5px
22px
rgba
(
36
,
16
,
95
,
.07
)}
.composer
textarea
{
width
:
100%
;
padding
:
2px
4px
;
resize
:
none
;
color
:
var
(
--text
);
background
:
transparent
;
border
:
0
;
outline
:
none
;
font-size
:
11px
;
line-height
:
1.55
}
.composer
textarea
::placeholder
{
color
:
#a5a7b0
}
.composer
>
div
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
}
.composer
>
div
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--muted
);
font-size
:
9px
}
.online-dot
{
width
:
6px
;
height
:
6px
;
border-radius
:
50%
;
background
:
#20b486
}
.composer
button
{
display
:
grid
;
place-items
:
center
;
width
:
29px
;
height
:
29px
;
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
;
border-radius
:
8px
}
.history-list
{
flex
:
1
1
0
;
min-height
:
0
;
overflow
:
auto
;
padding
:
16px
16px
22px
}
.environment-versions
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
8px
;
margin-bottom
:
22px
}
.environment-versions
>
button
{
min-width
:
0
;
padding
:
12px
;
text-align
:
left
;
background
:
#fafafd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
12px
}
.environment-versions
>
button
:hover
,
.environment-versions
>
button
.active
{
border-color
:
#bca8ff
;
background
:
#faf8ff
;
box-shadow
:
0
5px
18px
rgba
(
112
,
40
,
255
,
.08
)}
.environment-versions
>
button
.production
:hover
,
.environment-versions
>
button
.production.active
{
border-color
:
#9bd8c6
;
background
:
#f4fcf9
;
box-shadow
:
0
5px
18px
rgba
(
32
,
180
,
134
,
.08
)}
.environment-versions
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
750
}
.environment-versions
span
>
em
{
margin-left
:
auto
;
padding
:
2px
4px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
4px
;
font-size
:
7px
;
font-style
:
normal
}
.environment-versions
.production
span
>
em
{
color
:
#13765b
;
background
:
#e7f8f2
}
.environment-versions
strong
,
.environment-versions
small
{
display
:
block
}
.environment-versions
strong
{
margin-top
:
13px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
13px
;
letter-spacing
:
.02em
}
.environment-versions
small
{
margin-top
:
5px
;
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.history-heading
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
18px
}
.history-heading
h3
{
margin
:
0
0
4px
;
font-size
:
14px
}
.history-heading
p
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.history-heading
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
8px
;
font-size
:
10px
;
font-weight
:
800
}
.history-list
article
{
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
7px
;
min-height
:
69px
}
.history-list
article
.environment-version
{
margin
:
0
-5px
;
padding
:
0
5px
;
background
:
#fbfaff
;
border-radius
:
10px
}
.timeline
{
display
:
flex
;
align-items
:
center
;
flex-direction
:
column
}
.timeline
i
{
display
:
grid
;
place-items
:
center
;
width
:
17px
;
height
:
17px
;
color
:
white
;
background
:
#d6d7df
;
border
:
3px
solid
white
;
box-shadow
:
0
0
0
1px
#d6d7df
;
border-radius
:
50%
;
font-style
:
normal
}
.history-list
article
.environment-version
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.timeline
span
{
width
:
1px
;
flex
:
1
;
background
:
var
(
--divider
)}
.commit-info
{
min-width
:
0
;
padding-bottom
:
18px
}
.commit-info
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.preview-badge
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.commit-info
.production-badge
{
color
:
#13765b
;
background
:
#e7f8f2
}
.commit-info
p
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
5px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.commit-info
code
{
font-size
:
9px
}
.history-actions
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
4px
}
.history-actions
button
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
padding
:
0
;
color
:
var
(
--muted
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
7px
}
.history-actions
button
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-list
{
flex
:
1
1
0
;
min-height
:
0
;
overflow
:
auto
;
padding
:
16px
16px
22px
}
.environment-versions
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
8px
;
margin-bottom
:
22px
}
.environment-versions
>
button
{
min-width
:
0
;
padding
:
12px
;
text-align
:
left
;
background
:
#fafafd
;
border
:
1px
solid
var
(
--border
);
border-radius
:
12px
}
.environment-versions
>
button
:hover
,
.environment-versions
>
button
.active
{
border-color
:
#bca8ff
;
background
:
#faf8ff
;
box-shadow
:
0
5px
18px
rgba
(
112
,
40
,
255
,
.08
)}
.environment-versions
>
button
.production
:hover
,
.environment-versions
>
button
.production.active
{
border-color
:
#9bd8c6
;
background
:
#f4fcf9
;
box-shadow
:
0
5px
18px
rgba
(
32
,
180
,
134
,
.08
)}
.environment-versions
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
color
:
var
(
--text-2
);
font-size
:
9px
;
font-weight
:
750
}
.environment-versions
span
>
em
{
margin-left
:
auto
;
padding
:
2px
4px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
4px
;
font-size
:
7px
;
font-style
:
normal
}
.environment-versions
.production
span
>
em
{
color
:
#13765b
;
background
:
#e7f8f2
}
.environment-versions
strong
,
.environment-versions
small
{
display
:
block
}
.environment-versions
strong
{
margin-top
:
13px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
13px
;
letter-spacing
:
.02em
}
.environment-versions
small
{
margin-top
:
5px
;
overflow
:
hidden
;
color
:
var
(
--muted
);
font-size
:
8px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.history-heading
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
18px
}
.history-heading
h3
{
margin
:
0
0
4px
;
font-size
:
14px
}
.history-heading
p
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.history-heading
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-radius
:
8px
;
font-size
:
10px
;
font-weight
:
800
}
.history-list
article
{
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
7px
;
min-height
:
69px
}
.history-list
article
.environment-version
{
margin
:
0
-5px
;
padding
:
0
5px
;
background
:
#fbfaff
;
border-radius
:
10px
}
.timeline
{
display
:
flex
;
align-items
:
center
;
flex-direction
:
column
}
.timeline
i
{
display
:
grid
;
place-items
:
center
;
width
:
17px
;
height
:
17px
;
color
:
white
;
background
:
#d6d7df
;
border
:
3px
solid
white
;
box-shadow
:
0
0
0
1px
#d6d7df
;
border-radius
:
50%
;
font-style
:
normal
}
.history-list
article
.environment-version
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.timeline
span
{
width
:
1px
;
flex
:
1
;
background
:
var
(
--divider
)}
.commit-info
{
min-width
:
0
;
padding-bottom
:
18px
}
.commit-info
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.preview-badge
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.commit-info
.production-badge
{
color
:
#13765b
;
background
:
#e7f8f2
}
.commit-info
p
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin
:
5px
0
0
;
color
:
var
(
--muted
);
font-size
:
9px
}
.commit-info
code
{
font-size
:
9px
}
.history-actions
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
4px
}
.history-actions
button
{
display
:
grid
;
place-items
:
center
;
width
:
25px
;
height
:
25px
;
padding
:
0
;
color
:
var
(
--muted
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
7px
}
.history-actions
button
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.preview-shell
{
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
}
.preview-toolbar
{
min-height
:
66px
;
display
:
grid
;
grid-template-columns
:
minmax
(
190px
,
1
fr
)
auto
minmax
(
310px
,
1
fr
);
grid-template-areas
:
"title modes actions"
;
align-items
:
center
;
gap
:
12px
;
padding
:
0
18px
;
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.preview-title
{
grid-area
:
title
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
min-width
:
0
}
.preview-title
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
}
.preview-title
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.preview-title
em
{
flex
:
0
0
auto
;
padding
:
3px
6px
;
color
:
#7957d5
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
8px
;
font-style
:
normal
;
font-weight
:
750
}
.preview-title
em
.production
{
color
:
#13765b
;
background
:
#e7f8f2
}
.status-dot
{
flex
:
0
0
auto
;
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#7957d5
;
box-shadow
:
0
0
0
4px
rgba
(
112
,
40
,
255
,
.1
)}
.status-dot.production
{
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.1
)}
.build-status
{
flex
:
0
0
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
padding
:
5px
7px
;
color
:
#188866
;
background
:
#ebfbf5
;
border-radius
:
7px
;
font-size
:
8px
;
font-weight
:
750
}
.build-status.failed
{
color
:
#b42318
;
background
:
#fff0ee
}
.build-status.unpublished
{
color
:
#986600
;
background
:
#fff8e5
}
.preview-modes
{
grid-area
:
modes
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
}
.environment-toggle
,
.device-toggle
{
display
:
flex
;
padding
:
3px
;
background
:
#f2f2f6
;
border-radius
:
8px
}
.environment-toggle
button
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
750
}
.environment-toggle
button
.active
,
.device-toggle
button
.active
{
color
:
var
(
--text
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
36
,
16
,
95
,
.1
)}
.device-toggle
button
{
display
:
grid
;
place-items
:
center
;
width
:
31px
;
height
:
27px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
}
.toolbar-actions
{
grid-area
:
actions
;
display
:
flex
;
justify-content
:
flex-end
;
gap
:
6px
;
min-width
:
0
}
.toolbar-actions
button
,
.toolbar-actions
a
{
height
:
31px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
font-size
:
8px
;
font-weight
:
750
;
white-space
:
nowrap
}
.toolbar-actions
button
:hover:not
(
:disabled
),
.toolbar-actions
a
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.toolbar-actions
.environment-link.production-link
{
color
:
#13765b
}
.toolbar-actions
.environment-link
:disabled
{
color
:
#a8a9b0
;
background
:
#f5f5f7
}
.toolbar-actions
.rebuild-button
{
color
:
var
(
--primary
);
border-color
:
#d8ccff
;
background
:
#faf8ff
}
.toolbar-actions
.publish-button
{
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
}
.toolbar-actions
.publish-button
:hover:not
(
:disabled
)
{
color
:
white
;
background
:
var
(
--gradient
);
box-shadow
:
0
7px
17px
rgba
(
112
,
40
,
255
,
.2
)}
.toolbar-actions
.publish-button
:disabled
{
color
:
#9a9da8
;
background
:
#efeff4
;
border
:
1px
solid
#e1e1e7
;
box-shadow
:
none
}
.preview-shell
{
min-height
:
0
;
min-width
:
0
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
}
.preview-toolbar
{
min-height
:
66px
;
display
:
grid
;
grid-template-columns
:
minmax
(
190px
,
1
fr
)
auto
minmax
(
310px
,
1
fr
);
grid-template-areas
:
"title modes actions"
;
align-items
:
center
;
gap
:
12px
;
padding
:
0
18px
;
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.preview-title
{
grid-area
:
title
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
min-width
:
0
}
.preview-title
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
min-width
:
0
}
.preview-title
strong
{
overflow
:
hidden
;
font-size
:
12px
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.preview-title
em
{
flex
:
0
0
auto
;
padding
:
3px
6px
;
color
:
#7957d5
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
8px
;
font-style
:
normal
;
font-weight
:
750
}
.preview-title
em
.production
{
color
:
#13765b
;
background
:
#e7f8f2
}
.status-dot
{
flex
:
0
0
auto
;
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#7957d5
;
box-shadow
:
0
0
0
4px
rgba
(
112
,
40
,
255
,
.1
)}
.status-dot.production
{
background
:
#20b486
;
box-shadow
:
0
0
0
4px
rgba
(
32
,
180
,
134
,
.1
)}
.build-status
{
flex
:
0
0
auto
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
padding
:
5px
7px
;
color
:
#188866
;
background
:
#ebfbf5
;
border-radius
:
7px
;
font-size
:
8px
;
font-weight
:
750
}
.build-status.failed
{
color
:
#b42318
;
background
:
#fff0ee
}
.build-status.unpublished
{
color
:
#986600
;
background
:
#fff8e5
}
.preview-modes
{
grid-area
:
modes
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
}
.environment-toggle
,
.device-toggle
{
display
:
flex
;
padding
:
3px
;
background
:
#f2f2f6
;
border-radius
:
8px
}
.environment-toggle
button
{
height
:
27px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
;
font-size
:
8px
;
font-weight
:
750
}
.environment-toggle
button
.active
,
.device-toggle
button
.active
{
color
:
var
(
--text
);
background
:
#fff
;
box-shadow
:
0
2px
7px
rgba
(
36
,
16
,
95
,
.1
)}
.device-toggle
button
{
display
:
grid
;
place-items
:
center
;
width
:
31px
;
height
:
27px
;
color
:
var
(
--muted
);
background
:
transparent
;
border
:
0
;
border-radius
:
6px
}
.toolbar-actions
{
grid-area
:
actions
;
display
:
flex
;
justify-content
:
flex-end
;
gap
:
6px
;
min-width
:
0
}
.toolbar-actions
button
,
.toolbar-actions
a
{
height
:
31px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
0
9px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
);
border-radius
:
8px
;
font-size
:
8px
;
font-weight
:
750
;
white-space
:
nowrap
}
.toolbar-actions
button
:hover:not
(
:disabled
),
.toolbar-actions
a
:hover
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.toolbar-actions
.environment-link.production-link
{
color
:
#13765b
}
.toolbar-actions
.environment-link
:disabled
{
color
:
#a8a9b0
;
background
:
#f5f5f7
}
.toolbar-actions
.rebuild-button
{
color
:
var
(
--primary
);
border-color
:
#d8ccff
;
background
:
#faf8ff
}
.toolbar-actions
.undo-button
{
color
:
#9a6100
;
border-color
:
#ecd8ad
;
background
:
#fffaf0
}
.toolbar-actions
.publish-button
{
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
}
.toolbar-actions
.publish-button
:hover:not
(
:disabled
)
{
color
:
white
;
background
:
var
(
--gradient
);
box-shadow
:
0
7px
17px
rgba
(
112
,
40
,
255
,
.2
)}
.toolbar-actions
.publish-button
:disabled
{
color
:
#9a9da8
;
background
:
#efeff4
;
border
:
1px
solid
#e1e1e7
;
box-shadow
:
none
}
.history-edit-lock
{
flex
:
0
0
auto
;
margin
:
0
14px
10px
;
padding
:
9px
11px
;
color
:
#6f4b00
;
background
:
#fff8e7
;
border
:
1px
solid
#f0dfb8
;
border-radius
:
10px
;
font-size
:
9px
;
line-height
:
1.5
}
.history-edit-lock
>
div
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
7px
}
.history-edit-lock
svg
{
flex
:
0
0
auto
;
margin-top
:
1px
}
.history-panel-lock
{
margin
:
0
0
14px
}
.history-list
article
{
display
:
block
;
min-height
:
0
;
margin-bottom
:
5px
}
.history-list
article
.selected
{
background
:
#faf8ff
;
border-radius
:
11px
}
.history-row
{
width
:
100%
;
min-width
:
0
;
display
:
grid
;
grid-template-columns
:
24px
minmax
(
0
,
1
fr
)
auto
;
gap
:
8px
;
padding
:
9px
8px
5px
;
color
:
var
(
--text
);
text-align
:
left
;
background
:
transparent
;
border
:
1px
solid
transparent
;
border-radius
:
11px
}
.history-list
article
.selected
.history-row
{
border-color
:
#ded4ff
}
.history-row
.timeline
{
min-height
:
49px
}
.history-list
article
.selected
.timeline
i
{
background
:
var
(
--primary
);
box-shadow
:
0
0
0
1px
var
(
--primary
)}
.history-row
.commit-info
{
display
:
block
;
min-width
:
0
;
padding
:
0
0
10px
}
.commit-info
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
min-width
:
0
}
.commit-info
>
span
>
strong
{
min-width
:
0
;
overflow
:
hidden
;
font-size
:
10px
;
line-height
:
1.4
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.commit-info
em
{
flex
:
0
0
auto
;
padding
:
2px
5px
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.commit-info
.current-badge
{
color
:
#775400
;
background
:
#fff2c7
}
.commit-info
>
small
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
margin-top
:
6px
;
color
:
var
(
--muted
);
font-size
:
8px
}
.commit-info
>
small
code
{
font-size
:
8px
}
.history-row-state
,
.history-preview-button
{
align-self
:
start
;
min-height
:
24px
;
padding
:
4px
7px
;
border-radius
:
6px
;
font-size
:
7px
;
font-weight
:
750
}
.history-row-state
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.history-preview-button
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
color
:
var
(
--text-2
);
background
:
#fff
;
border
:
1px
solid
var
(
--border
)}
.history-preview-button
:hover:not
(
:disabled
)
{
color
:
var
(
--primary
);
background
:
var
(
--soft
);
border-color
:
#d8ccff
}
.version-state-strip
{
flex
:
0
0
70px
;
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
background
:
#fff
;
border-bottom
:
1px
solid
var
(
--border
)}
.version-state-strip
article
{
min-width
:
0
;
padding
:
10px
16px
;
border-right
:
1px
solid
var
(
--divider
)}
.version-state-strip
article
:last-child
{
border-right
:
0
}
.version-state-strip
article
>
span
,
.version-state-strip
strong
,
.version-state-strip
small
{
display
:
block
;
overflow
:
hidden
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
}
.version-state-strip
article
>
span
{
color
:
var
(
--muted
);
font-size
:
8px
;
font-weight
:
750
}
.version-state-strip
article
>
span
em
{
margin-left
:
4px
;
padding
:
2px
5px
;
color
:
#6a4bb8
;
background
:
#f2edff
;
border-radius
:
5px
;
font-size
:
7px
;
font-style
:
normal
}
.version-state-strip
strong
{
margin-top
:
5px
;
font-family
:
ui-monospace
,
SFMono-Regular
,
monospace
;
font-size
:
11px
}
.version-state-strip
small
{
margin-top
:
3px
;
color
:
var
(
--muted
);
font-size
:
7px
}
.version-state-strip
article
.error
{
background
:
#fffafa
}
.version-state-strip
article
.error
>
span
em
{
color
:
#b42318
;
background
:
#fff0ee
}
.version-state-strip
article
.production
{
background
:
#fbfffd
}
.version-state-strip
article
.production
strong
{
color
:
#13765b
}
.browser-stage
{
position
:
relative
;
flex
:
1
;
min-height
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
25px
;
background
:
#eff0f5
;
overflow
:
auto
}
.browser-frame
{
position
:
relative
;
width
:
100%
;
height
:
100%
;
overflow
:
hidden
;
background
:
#fff
;
border
:
1px
solid
#dfe0e7
;
border-radius
:
11px
;
box-shadow
:
0
15px
45px
rgba
(
30
,
24
,
53
,
.1
);
transition
:
.35s
ease
}
.browser-frame.mobile
{
width
:
390px
;
max-height
:
760px
}
.browser-chrome
{
height
:
31px
;
display
:
grid
;
grid-template-columns
:
70px
1
fr
70px
;
align-items
:
center
;
padding
:
0
10px
;
background
:
#fafafd
;
border-bottom
:
1px
solid
#ebeaf0
}
.browser-chrome
>
div
{
display
:
flex
;
gap
:
5px
}
.browser-chrome
i
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#e1e1e7
}
.browser-chrome
i
:first-child
{
background
:
#ff8b86
}
.browser-chrome
i
:nth-child
(
2
)
{
background
:
#ffd36b
}
.browser-chrome
i
:nth-child
(
3
)
{
background
:
#71d19a
}
.browser-chrome
span
{
justify-self
:
center
;
width
:
min
(
390px
,
80%
);
padding
:
4px
10px
;
text-align
:
center
;
color
:
#9a9ca6
;
background
:
#f0f0f4
;
border-radius
:
5px
;
font-size
:
8px
}
.browser-chrome
>
svg
{
justify-self
:
end
;
color
:
#a7a9b3
}
.browser-frame
iframe
{
display
:
block
;
width
:
100%
;
height
:
calc
(
100%
-
31px
);
border
:
0
;
background
:
#fff
}
.production-empty
{
height
:
calc
(
100%
-
31px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
padding
:
30px
;
text-align
:
center
;
background
:
linear-gradient
(
180deg
,
#fbfffd
,
#f7faf9
)}
.production-empty
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
52px
;
height
:
52px
;
color
:
#168064
;
background
:
#e8f8f2
;
border-radius
:
16px
}
.production-empty
strong
{
margin-top
:
17px
;
font-size
:
15px
}
.production-empty
p
{
max-width
:
360px
;
margin
:
8px
0
20px
;
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.7
}
.production-empty
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
0
14px
;
color
:
white
;
background
:
linear-gradient
(
135deg
,
#15936e
,
#20b486
);
border
:
0
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
800
}
.preview-busy
{
position
:
absolute
;
inset
:
31px
0
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
gap
:
8px
;
color
:
var
(
--primary
);
background
:
rgba
(
248
,
248
,
252
,
.72
);
backdrop-filter
:
blur
(
4px
)}
.preview-busy
strong
{
color
:
var
(
--text
);
font-size
:
13px
}
.preview-busy
span
{
color
:
var
(
--muted
);
font-size
:
10px
}
.preview-footer
{
height
:
29px
;
padding
:
0
22px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
color
:
var
(
--muted
);
background
:
#fff
;
border-top
:
1px
solid
var
(
--divider
);
font-size
:
8px
}
.preview-footer
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.browser-stage
{
position
:
relative
;
flex
:
1
;
min-height
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
25px
;
background
:
#eff0f5
;
overflow
:
auto
}
.browser-frame
{
position
:
relative
;
width
:
100%
;
height
:
100%
;
overflow
:
hidden
;
background
:
#fff
;
border
:
1px
solid
#dfe0e7
;
border-radius
:
11px
;
box-shadow
:
0
15px
45px
rgba
(
30
,
24
,
53
,
.1
);
transition
:
.35s
ease
}
.browser-frame.mobile
{
width
:
390px
;
max-height
:
760px
}
.browser-chrome
{
height
:
31px
;
display
:
grid
;
grid-template-columns
:
70px
1
fr
70px
;
align-items
:
center
;
padding
:
0
10px
;
background
:
#fafafd
;
border-bottom
:
1px
solid
#ebeaf0
}
.browser-chrome
>
div
{
display
:
flex
;
gap
:
5px
}
.browser-chrome
i
{
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
#e1e1e7
}
.browser-chrome
i
:first-child
{
background
:
#ff8b86
}
.browser-chrome
i
:nth-child
(
2
)
{
background
:
#ffd36b
}
.browser-chrome
i
:nth-child
(
3
)
{
background
:
#71d19a
}
.browser-chrome
span
{
justify-self
:
center
;
width
:
min
(
390px
,
80%
);
padding
:
4px
10px
;
text-align
:
center
;
color
:
#9a9ca6
;
background
:
#f0f0f4
;
border-radius
:
5px
;
font-size
:
8px
}
.browser-chrome
>
svg
{
justify-self
:
end
;
color
:
#a7a9b3
}
.browser-frame
iframe
{
display
:
block
;
width
:
100%
;
height
:
calc
(
100%
-
31px
);
border
:
0
;
background
:
#fff
}
.production-empty
{
height
:
calc
(
100%
-
31px
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
padding
:
30px
;
text-align
:
center
;
background
:
linear-gradient
(
180deg
,
#fbfffd
,
#f7faf9
)}
.production-empty
>
span
{
display
:
grid
;
place-items
:
center
;
width
:
52px
;
height
:
52px
;
color
:
#168064
;
background
:
#e8f8f2
;
border-radius
:
16px
}
.production-empty
strong
{
margin-top
:
17px
;
font-size
:
15px
}
.production-empty
p
{
max-width
:
360px
;
margin
:
8px
0
20px
;
color
:
var
(
--muted
);
font-size
:
10px
;
line-height
:
1.7
}
.production-empty
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
padding
:
0
14px
;
color
:
white
;
background
:
linear-gradient
(
135deg
,
#15936e
,
#20b486
);
border
:
0
;
border-radius
:
9px
;
font-size
:
10px
;
font-weight
:
800
}
.preview-busy
{
position
:
absolute
;
inset
:
31px
0
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
gap
:
8px
;
color
:
var
(
--primary
);
background
:
rgba
(
248
,
248
,
252
,
.72
);
backdrop-filter
:
blur
(
4px
)}
.preview-busy
strong
{
color
:
var
(
--text
);
font-size
:
13px
}
.preview-busy
span
{
color
:
var
(
--muted
);
font-size
:
10px
}
.preview-footer
{
height
:
29px
;
padding
:
0
22px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
color
:
var
(
--muted
);
background
:
#fff
;
border-top
:
1px
solid
var
(
--divider
);
font-size
:
8px
}
.preview-footer
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
@media
(
max-width
:
1100px
){
.create-layout
{
grid-template-columns
:
1
fr
;
gap
:
48px
;
padding-top
:
40px
}
.create-intro
{
text-align
:
center
}
.create-intro
>
p
,
.create-intro
h1
{
margin-left
:
auto
;
margin-right
:
auto
}
.intro-tag
{
margin
:
auto
}
.feature-row
{
max-width
:
620px
;
margin-left
:
auto
;
margin-right
:
auto
}
.create-card-wrap
{
justify-content
:
center
}
.workspace
{
grid-template-columns
:
58px
340px
1
fr
}}
@media
(
max-width
:
1100px
){
.create-layout
{
grid-template-columns
:
1
fr
;
gap
:
48px
;
padding-top
:
40px
}
.create-intro
{
text-align
:
center
}
.create-intro
>
p
,
.create-intro
h1
{
margin-left
:
auto
;
margin-right
:
auto
}
.intro-tag
{
margin
:
auto
}
.feature-row
{
max-width
:
620px
;
margin-left
:
auto
;
margin-right
:
auto
}
.create-card-wrap
{
justify-content
:
center
}
.workspace
{
grid-template-columns
:
58px
340px
1
fr
}}
@media
(
max-width
:
1250px
)
and
(
min-width
:
801px
){
.preview-toolbar
{
height
:
96px
;
grid-template-columns
:
minmax
(
160px
,
1
fr
)
auto
;
grid-template-rows
:
42px
42px
;
grid-template-areas
:
"title actions"
"modes modes"
;
gap
:
0
10px
;
padding
:
6px
12px
}
.preview-modes
{
justify-content
:
center
}
.toolbar-actions
.environment-link
{
width
:
31px
;
padding
:
0
}
.toolbar-actions
.environment-link
>
span
{
display
:
none
}}
@media
(
max-width
:
1250px
)
and
(
min-width
:
801px
){
.preview-toolbar
{
height
:
96px
;
grid-template-columns
:
minmax
(
160px
,
1
fr
)
auto
;
grid-template-rows
:
42px
42px
;
grid-template-areas
:
"title actions"
"modes modes"
;
gap
:
0
10px
;
padding
:
6px
12px
}
.preview-modes
{
justify-content
:
center
}
.toolbar-actions
.environment-link
{
width
:
31px
;
padding
:
0
}
.toolbar-actions
.environment-link
>
span
{
display
:
none
}}
...
...
shared/src/index.ts
View file @
10c7988e
...
@@ -22,6 +22,8 @@ export interface SiteInfo {
...
@@ -22,6 +22,8 @@ export interface SiteInfo {
previewUrl
:
string
;
previewUrl
:
string
;
productionUrl
?:
string
;
productionUrl
?:
string
;
currentCommit
:
string
;
currentCommit
:
string
;
previewCommit
?:
string
;
previousCommit
?:
string
;
publishedCommit
?:
string
;
publishedCommit
?:
string
;
publishedAt
?:
string
;
publishedAt
?:
string
;
publishStatus
:
PublishStatus
;
publishStatus
:
PublishStatus
;
...
@@ -61,3 +63,8 @@ export interface PublishResult {
...
@@ -61,3 +63,8 @@ export interface PublishResult {
commit
:
string
;
commit
:
string
;
publishedAt
:
string
;
publishedAt
:
string
;
}
}
export
interface
PreviewVersionResult
{
previewUrl
:
string
;
commit
:
string
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment