proto-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
TOOL_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
# shellcheck source=lib/source-path.sh
|
|
6
|
+
source "$TOOL_ROOT/scripts/lib/source-path.sh"
|
|
7
|
+
HOST_ROOT="$(resolve_host_root "$TOOL_ROOT")"
|
|
8
|
+
cd "$HOST_ROOT"
|
|
9
|
+
|
|
10
|
+
if [[ -z "${VERCEL_PROJECT:-}" && -f "$HOST_ROOT/package.json" ]]; then
|
|
11
|
+
VERCEL_PROJECT="$(node -pe "require('$HOST_ROOT/package.json').name" 2>/dev/null || true)"
|
|
12
|
+
fi
|
|
13
|
+
VERCEL_PROJECT="${VERCEL_PROJECT:-}"
|
|
14
|
+
PUSH=1
|
|
15
|
+
OPEN=0
|
|
16
|
+
SLUG=""
|
|
17
|
+
|
|
18
|
+
usage() {
|
|
19
|
+
cat <<EOF
|
|
20
|
+
Usage: $(basename "$0") [options] <prototype-slug>
|
|
21
|
+
|
|
22
|
+
Push the current branch, wait for the Vercel preview deployment, and copy a
|
|
23
|
+
shareable link to the prototype (e.g. /prototypes/example-feature).
|
|
24
|
+
|
|
25
|
+
Before pushing, stages all changes (git add -A) and commits them when needed.
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--no-push Skip git push (use the current branch HEAD on GitHub)
|
|
29
|
+
--open Open the shareable link in your browser
|
|
30
|
+
--project <name> Vercel project to use (default: from VERCEL_PROJECT or package.json name)
|
|
31
|
+
-h, --help Show this help message
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
pnpm share-prototype example-feature
|
|
35
|
+
pnpm share-prototype --no-push example-feature
|
|
36
|
+
|
|
37
|
+
Requires: git, gh (authenticated), and network access.
|
|
38
|
+
EOF
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while [[ $# -gt 0 ]]; do
|
|
42
|
+
case "$1" in
|
|
43
|
+
--no-push)
|
|
44
|
+
PUSH=0
|
|
45
|
+
shift
|
|
46
|
+
;;
|
|
47
|
+
--open)
|
|
48
|
+
OPEN=1
|
|
49
|
+
shift
|
|
50
|
+
;;
|
|
51
|
+
--project)
|
|
52
|
+
[[ $# -ge 2 ]] || {
|
|
53
|
+
echo "Missing value for --project" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
}
|
|
56
|
+
VERCEL_PROJECT="$2"
|
|
57
|
+
shift 2
|
|
58
|
+
;;
|
|
59
|
+
-h | --help)
|
|
60
|
+
usage
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
-*)
|
|
64
|
+
echo "Unknown option: $1" >&2
|
|
65
|
+
usage >&2
|
|
66
|
+
exit 1
|
|
67
|
+
;;
|
|
68
|
+
*)
|
|
69
|
+
if [[ -n "$SLUG" ]]; then
|
|
70
|
+
echo "Unexpected argument: $1" >&2
|
|
71
|
+
usage >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
SLUG="$1"
|
|
75
|
+
shift
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
if [[ -z "$SLUG" ]]; then
|
|
81
|
+
usage >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if [[ -z "$VERCEL_PROJECT" ]]; then
|
|
86
|
+
echo "Set VERCEL_PROJECT or pass --project <name>." >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
require_command() {
|
|
91
|
+
if ! command -v "$1" >/dev/null 2>&1; then
|
|
92
|
+
echo "Missing required command: $1" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
require_command git
|
|
98
|
+
require_command gh
|
|
99
|
+
|
|
100
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
101
|
+
echo "gh is not authenticated. Run: gh auth login" >&2
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
remote_url="$(git remote get-url origin 2>/dev/null || true)"
|
|
106
|
+
if [[ -z "$remote_url" ]]; then
|
|
107
|
+
echo "No git remote named origin found." >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+)(\.git)?$ ]]; then
|
|
112
|
+
REPO_OWNER="${BASH_REMATCH[1]}"
|
|
113
|
+
REPO_NAME="${BASH_REMATCH[2]%.git}"
|
|
114
|
+
else
|
|
115
|
+
echo "Could not parse GitHub owner/repo from origin: $remote_url" >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
branch="$(git branch --show-current)"
|
|
120
|
+
if [[ -z "$branch" ]]; then
|
|
121
|
+
echo "Detached HEAD — checkout a branch before sharing." >&2
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
copy_to_clipboard() {
|
|
126
|
+
local text="$1"
|
|
127
|
+
if command -v pbcopy >/dev/null 2>&1; then
|
|
128
|
+
printf '%s' "$text" | pbcopy
|
|
129
|
+
elif command -v wl-copy >/dev/null 2>&1; then
|
|
130
|
+
printf '%s' "$text" | wl-copy
|
|
131
|
+
elif command -v xclip >/dev/null 2>&1; then
|
|
132
|
+
printf '%s' "$text" | xclip -selection clipboard
|
|
133
|
+
else
|
|
134
|
+
echo "Could not copy to clipboard (install pbcopy, wl-copy, or xclip)." >&2
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
extract_branch_preview_host() {
|
|
140
|
+
local sha="$1"
|
|
141
|
+
local summary
|
|
142
|
+
|
|
143
|
+
summary="$(
|
|
144
|
+
gh api \
|
|
145
|
+
"repos/$REPO_OWNER/$REPO_NAME/commits/$sha/check-runs?per_page=30" \
|
|
146
|
+
--jq '.check_runs[] | select(.name == "Vercel Preview Comments") | .output.summary' \
|
|
147
|
+
2>/dev/null | head -1
|
|
148
|
+
)"
|
|
149
|
+
|
|
150
|
+
if [[ -z "$summary" ]]; then
|
|
151
|
+
return 1
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if [[ "$summary" =~ vercel\.live/open-feedback/([a-z0-9.-]+\.vercel\.app) ]]; then
|
|
155
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [[ "$summary" =~ (https?://)?([a-z0-9.-]+\.vercel\.app) ]]; then
|
|
160
|
+
printf '%s\n' "${BASH_REMATCH[2]}"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
return 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
extract_deployment_preview_url() {
|
|
168
|
+
local sha="$1"
|
|
169
|
+
local deployment_id environment_url
|
|
170
|
+
|
|
171
|
+
deployment_id="$(
|
|
172
|
+
gh api \
|
|
173
|
+
"repos/$REPO_OWNER/$REPO_NAME/deployments?sha=$sha&per_page=20" \
|
|
174
|
+
--jq ".[] | select(.environment == \"Preview – $VERCEL_PROJECT\") | .id" \
|
|
175
|
+
2>/dev/null | head -1
|
|
176
|
+
)"
|
|
177
|
+
|
|
178
|
+
if [[ -z "$deployment_id" ]]; then
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
environment_url="$(
|
|
183
|
+
gh api \
|
|
184
|
+
"repos/$REPO_OWNER/$REPO_NAME/deployments/$deployment_id/statuses" \
|
|
185
|
+
--jq '.[] | select(.state == "success" and .environment_url != null) | .environment_url' \
|
|
186
|
+
2>/dev/null | head -1
|
|
187
|
+
)"
|
|
188
|
+
|
|
189
|
+
if [[ -z "$environment_url" ]]; then
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
printf '%s\n' "$environment_url"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
resolve_preview_origin() {
|
|
197
|
+
local sha="$1"
|
|
198
|
+
local host origin
|
|
199
|
+
|
|
200
|
+
if host="$(extract_branch_preview_host "$sha")"; then
|
|
201
|
+
printf 'https://%s\n' "$host"
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if origin="$(extract_deployment_preview_url "$sha")"; then
|
|
206
|
+
origin="${origin%/}"
|
|
207
|
+
printf '%s\n' "$origin"
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
return 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
wait_for_preview_origin() {
|
|
215
|
+
local sha="$1"
|
|
216
|
+
local attempt=0
|
|
217
|
+
local max_attempts=60
|
|
218
|
+
local origin=""
|
|
219
|
+
|
|
220
|
+
while (( attempt < max_attempts )); do
|
|
221
|
+
if origin="$(resolve_preview_origin "$sha")"; then
|
|
222
|
+
printf '%s\n' "$origin"
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
sleep 10
|
|
227
|
+
((attempt += 1))
|
|
228
|
+
done
|
|
229
|
+
|
|
230
|
+
echo "Timed out waiting for Vercel preview deployment." >&2
|
|
231
|
+
echo "Check deployments: https://github.com/$REPO_OWNER/$REPO_NAME/deployments" >&2
|
|
232
|
+
return 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
share_url=""
|
|
236
|
+
sha=""
|
|
237
|
+
|
|
238
|
+
if [[ "$PUSH" -eq 1 ]]; then
|
|
239
|
+
echo "Staging changes..."
|
|
240
|
+
git add -A
|
|
241
|
+
|
|
242
|
+
if ! git diff --cached --quiet; then
|
|
243
|
+
echo "Committing staged changes..."
|
|
244
|
+
git commit -m "Update prototype: $SLUG"
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
echo "Pushing branch: $branch"
|
|
248
|
+
git push -u origin HEAD
|
|
249
|
+
else
|
|
250
|
+
echo "Skipping push (using current branch HEAD on origin)."
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
sha="$(git rev-parse HEAD)"
|
|
254
|
+
echo "Commit: ${sha:0:7} on $branch"
|
|
255
|
+
echo "Building..." >&2
|
|
256
|
+
|
|
257
|
+
origin="$(wait_for_preview_origin "$sha")"
|
|
258
|
+
share_url="${origin%/}/prototypes/${SLUG}"
|
|
259
|
+
|
|
260
|
+
echo
|
|
261
|
+
echo "Shareable link:"
|
|
262
|
+
echo "$share_url"
|
|
263
|
+
echo
|
|
264
|
+
|
|
265
|
+
if copy_to_clipboard "$share_url"; then
|
|
266
|
+
echo "Copied to clipboard."
|
|
267
|
+
else
|
|
268
|
+
echo "Copy the link above manually."
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
if [[ "$OPEN" -eq 1 ]]; then
|
|
272
|
+
if command -v open >/dev/null 2>&1; then
|
|
273
|
+
open "$share_url"
|
|
274
|
+
elif command -v xdg-open >/dev/null 2>&1; then
|
|
275
|
+
xdg-open "$share_url"
|
|
276
|
+
else
|
|
277
|
+
echo "Could not open browser automatically." >&2
|
|
278
|
+
fi
|
|
279
|
+
fi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
TOOL_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
# shellcheck source=lib/source-path.sh
|
|
6
|
+
source "$TOOL_ROOT/scripts/lib/source-path.sh"
|
|
7
|
+
# shellcheck source=lib/host-config.sh
|
|
8
|
+
source "$TOOL_ROOT/scripts/lib/host-config.sh"
|
|
9
|
+
HOST_ROOT="$(resolve_host_root "$TOOL_ROOT")"
|
|
10
|
+
|
|
11
|
+
if ! load_host_config "$HOST_ROOT"; then
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if ! SOURCE="$(resolve_source_path "$HOST_ROOT")"; then
|
|
16
|
+
echo "Source app path not found." >&2
|
|
17
|
+
echo "Set SOURCE_PATH in .env.local (see .env.example), run pnpm link-source, or pass SOURCE=/path/to/source-app" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "Syncing from $SOURCE"
|
|
22
|
+
|
|
23
|
+
copy_file() {
|
|
24
|
+
local rel="$1"
|
|
25
|
+
mkdir -p "$HOST_ROOT/$(dirname "$rel")"
|
|
26
|
+
cp "$SOURCE/$rel" "$HOST_ROOT/$rel"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
copy_dir() {
|
|
30
|
+
local rel="$1"
|
|
31
|
+
mkdir -p "$HOST_ROOT/$rel"
|
|
32
|
+
cp -R "$SOURCE/$rel/." "$HOST_ROOT/$rel/"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
copy_glob() {
|
|
36
|
+
local glob="$1" dest matches
|
|
37
|
+
dest="$(dirname "$glob")"
|
|
38
|
+
mkdir -p "$HOST_ROOT/$dest"
|
|
39
|
+
# shellcheck disable=SC2206
|
|
40
|
+
matches=( "$SOURCE"/$glob )
|
|
41
|
+
if [[ -e "${matches[0]}" ]]; then
|
|
42
|
+
cp "${matches[@]}" "$HOST_ROOT/$dest/"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for rel in ${SYNC_FILES[@]+"${SYNC_FILES[@]}"}; do
|
|
47
|
+
copy_file "$rel"
|
|
48
|
+
done
|
|
49
|
+
for rel in ${SYNC_DIRS[@]+"${SYNC_DIRS[@]}"}; do
|
|
50
|
+
copy_dir "$rel"
|
|
51
|
+
done
|
|
52
|
+
for glob in ${SYNC_GLOBS[@]+"${SYNC_GLOBS[@]}"}; do
|
|
53
|
+
copy_glob "$glob"
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
# Re-apply platform Tailwind source scan (package self-reference; not in source app).
|
|
57
|
+
if ! grep -q '@source "../../packages/prototype' "$HOST_ROOT/src/app/globals.css"; then
|
|
58
|
+
if [[ "$OSTYPE" == darwin* ]]; then
|
|
59
|
+
sed -i '' '/^@import "tailwindcss";/a\
|
|
60
|
+
@source "../../packages/prototype/src/**/*.{js,ts,jsx,tsx}";
|
|
61
|
+
' "$HOST_ROOT/src/app/globals.css"
|
|
62
|
+
else
|
|
63
|
+
sed -i '/^@import "tailwindcss";/a @source "../../packages/prototype/src/**/*.{js,ts,jsx,tsx}";' "$HOST_ROOT/src/app/globals.css"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ "${SYNC_LOCAL_EXTENSIONS+set}" == set && "${#SYNC_LOCAL_EXTENSIONS[@]}" -gt 0 ]]; then
|
|
68
|
+
echo "Done. Re-apply local extensions:"
|
|
69
|
+
for ext in "${SYNC_LOCAL_EXTENSIONS[@]}"; do
|
|
70
|
+
echo " - $ext"
|
|
71
|
+
done
|
|
72
|
+
else
|
|
73
|
+
echo "Done."
|
|
74
|
+
fi
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ensures every UI component under src/prototypes/{slug}/ is registered in
|
|
4
|
+
* component-ids.ts and wraps its root JSX with PrototypeTarget/PrototypeComponent/PrototypeControl.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
resolveHostRoot,
|
|
10
|
+
resolveToolRoot,
|
|
11
|
+
} from "./lib/resolve-host-root.mjs";
|
|
12
|
+
|
|
13
|
+
const toolRoot = resolveToolRoot(import.meta.url);
|
|
14
|
+
const repoRoot = resolveHostRoot(toolRoot);
|
|
15
|
+
const prototypesDir = path.join(repoRoot, "src/prototypes");
|
|
16
|
+
|
|
17
|
+
function kebabCase(name) {
|
|
18
|
+
return name
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
20
|
+
.replace(/_/g, "-")
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listPrototypeSlugs() {
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(prototypesDir, { withFileTypes: true })
|
|
27
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
28
|
+
.map((d) => d.name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readRegistryIds(slug) {
|
|
32
|
+
const registryPath = path.join(prototypesDir, slug, "component-ids.ts");
|
|
33
|
+
if (!fs.existsSync(registryPath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const source = fs.readFileSync(registryPath, "utf8");
|
|
37
|
+
const match = source.match(
|
|
38
|
+
/(?:COMPONENT_IDS|\w+_COMPONENT_IDS)\s*=\s*\[([\s\S]*?)\]\s*as\s*const/,
|
|
39
|
+
);
|
|
40
|
+
if (!match) return null;
|
|
41
|
+
const ids = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
42
|
+
const prefixMatch = source.match(
|
|
43
|
+
/DYNAMIC_TARGET_PREFIXES\s*=\s*\[([\s\S]*?)\]\s*as\s*const/,
|
|
44
|
+
);
|
|
45
|
+
const prefixes = prefixMatch
|
|
46
|
+
? [...prefixMatch[1].matchAll(/"([^"]+)"/g)].map((m) => m[1])
|
|
47
|
+
: [];
|
|
48
|
+
return { ids: new Set(ids), prefixes };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectTsxFiles(slug) {
|
|
52
|
+
const root = path.join(prototypesDir, slug);
|
|
53
|
+
const files = [];
|
|
54
|
+
function walk(dir) {
|
|
55
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
56
|
+
const full = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) walk(full);
|
|
58
|
+
else if (entry.name.endsWith(".tsx")) files.push(full);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(root);
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fileBaseId(filePath, slug) {
|
|
66
|
+
const rel = path.relative(path.join(prototypesDir, slug), filePath);
|
|
67
|
+
const base = path.basename(rel, ".tsx");
|
|
68
|
+
if (base === "index") return "page";
|
|
69
|
+
return kebabCase(base);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findRenderableComponents(source, fileBase) {
|
|
73
|
+
const components = [];
|
|
74
|
+
const fnRegex =
|
|
75
|
+
/export\s+function\s+([A-Z][A-Za-z0-9]*)\s*\([^)]*\)\s*\{/g;
|
|
76
|
+
let m;
|
|
77
|
+
while ((m = fnRegex.exec(source)) !== null) {
|
|
78
|
+
const name = m[1];
|
|
79
|
+
const segment =
|
|
80
|
+
name === fileBase.replace(/-/g, "") ||
|
|
81
|
+
kebabCase(name) === fileBase
|
|
82
|
+
? fileBase
|
|
83
|
+
: `${fileBase}.${kebabCase(name)}`;
|
|
84
|
+
components.push({ name, expectedId: segment });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const defaultExport = /export\s+default\s+function\s+([A-Z][A-Za-z0-9]*)/.exec(
|
|
88
|
+
source,
|
|
89
|
+
);
|
|
90
|
+
if (defaultExport) {
|
|
91
|
+
components.push({ name: defaultExport[1], expectedId: "page" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return components;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasTargetWrapper(source) {
|
|
98
|
+
return (
|
|
99
|
+
source.includes("PrototypeTarget") ||
|
|
100
|
+
source.includes("PrototypeComponent") ||
|
|
101
|
+
source.includes("PrototypeControl")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let failed = false;
|
|
106
|
+
|
|
107
|
+
for (const slug of listPrototypeSlugs()) {
|
|
108
|
+
const registry = readRegistryIds(slug);
|
|
109
|
+
if (!registry) {
|
|
110
|
+
console.error(`[${slug}] Missing component-ids.ts`);
|
|
111
|
+
failed = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const file of collectTsxFiles(slug)) {
|
|
116
|
+
if (file.endsWith("component-ids.ts")) continue;
|
|
117
|
+
if (file.includes(`${path.sep}ui${path.sep}`)) continue;
|
|
118
|
+
if (file.includes(`${path.sep}_server${path.sep}`)) continue;
|
|
119
|
+
|
|
120
|
+
const source = fs.readFileSync(file, "utf8");
|
|
121
|
+
const fileBase = fileBaseId(file, slug);
|
|
122
|
+
const components = findRenderableComponents(source, fileBase);
|
|
123
|
+
|
|
124
|
+
if (components.length === 0) continue;
|
|
125
|
+
|
|
126
|
+
if (!hasTargetWrapper(source)) {
|
|
127
|
+
console.error(
|
|
128
|
+
`[${slug}] ${path.relative(repoRoot, file)}: no PrototypeTarget/PrototypeComponent/PrototypeControl wrapper`,
|
|
129
|
+
);
|
|
130
|
+
failed = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const { name, expectedId } of components) {
|
|
134
|
+
const allowed =
|
|
135
|
+
registry.ids.has(expectedId) ||
|
|
136
|
+
registry.prefixes.some((p) => expectedId.startsWith(p));
|
|
137
|
+
if (!allowed) {
|
|
138
|
+
console.error(
|
|
139
|
+
`[${slug}] ${name} in ${path.basename(file)}: expected registry id "${expectedId}"`,
|
|
140
|
+
);
|
|
141
|
+
failed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (failed) {
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log("Prototype component id verification passed.");
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ensures each prototype with a state map uses a single *-preview-states.ts
|
|
4
|
+
* registry that both the picker and canvas config import from.
|
|
5
|
+
*
|
|
6
|
+
* Runtime 1:1 parity (labels, variants, counts) is enforced when the registry
|
|
7
|
+
* module loads via definePreviewStateRegistry().
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
resolveHostRoot,
|
|
13
|
+
resolveToolRoot,
|
|
14
|
+
} from "./lib/resolve-host-root.mjs";
|
|
15
|
+
|
|
16
|
+
const toolRoot = resolveToolRoot(import.meta.url);
|
|
17
|
+
const repoRoot = resolveHostRoot(toolRoot);
|
|
18
|
+
const prototypesDir = path.join(repoRoot, "src/prototypes");
|
|
19
|
+
|
|
20
|
+
function listPrototypeSlugs() {
|
|
21
|
+
return fs
|
|
22
|
+
.readdirSync(prototypesDir, { withFileTypes: true })
|
|
23
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
24
|
+
.map((d) => d.name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function walkFiles(dir, predicate) {
|
|
28
|
+
const files = [];
|
|
29
|
+
|
|
30
|
+
function walk(current) {
|
|
31
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
32
|
+
const full = path.join(current, entry.name);
|
|
33
|
+
if (entry.isDirectory()) walk(full);
|
|
34
|
+
else if (predicate(full, entry.name)) files.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
walk(dir);
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findPreviewStateFiles(slug) {
|
|
43
|
+
return walkFiles(
|
|
44
|
+
path.join(prototypesDir, slug),
|
|
45
|
+
(_full, name) => name.endsWith("-preview-states.ts"),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findStateCanvasConfigFiles(slug) {
|
|
50
|
+
return walkFiles(
|
|
51
|
+
path.join(prototypesDir, slug),
|
|
52
|
+
(_full, name) => name.endsWith("-state-canvas-config.tsx"),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findStateSelectFiles(slug) {
|
|
57
|
+
return walkFiles(
|
|
58
|
+
path.join(prototypesDir, slug),
|
|
59
|
+
(_full, name) =>
|
|
60
|
+
name.endsWith(".tsx") &&
|
|
61
|
+
(name.includes("state-select") || name.includes("tab-select")),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fileUsesPreviewStateRegistry(filePath, registryBasename) {
|
|
66
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
67
|
+
const registryModule = registryBasename.replace(/\.ts$/, "");
|
|
68
|
+
return (
|
|
69
|
+
source.includes(registryModule) ||
|
|
70
|
+
source.includes("PREVIEW_STATE_REGISTRY") ||
|
|
71
|
+
source.includes("_PREVIEW_OPTIONS")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let failed = false;
|
|
76
|
+
|
|
77
|
+
for (const slug of listPrototypeSlugs()) {
|
|
78
|
+
const previewStateFiles = findPreviewStateFiles(slug);
|
|
79
|
+
const canvasConfigFiles = findStateCanvasConfigFiles(slug);
|
|
80
|
+
const stateSelectFiles = findStateSelectFiles(slug);
|
|
81
|
+
|
|
82
|
+
if (canvasConfigFiles.length > 0 && previewStateFiles.length === 0) {
|
|
83
|
+
console.error(
|
|
84
|
+
`[${slug}] Has state canvas config but no *-preview-states.ts registry.`,
|
|
85
|
+
);
|
|
86
|
+
failed = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (previewStateFiles.length === 0) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (previewStateFiles.length > 1) {
|
|
95
|
+
console.error(
|
|
96
|
+
`[${slug}] Expected one *-preview-states.ts file, found ${previewStateFiles.length}.`,
|
|
97
|
+
);
|
|
98
|
+
failed = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const registryFile = previewStateFiles[0];
|
|
102
|
+
const registryBasename = path.basename(registryFile);
|
|
103
|
+
const registrySource = fs.readFileSync(registryFile, "utf8");
|
|
104
|
+
|
|
105
|
+
if (!registrySource.includes("definePreviewStateRegistry")) {
|
|
106
|
+
console.error(
|
|
107
|
+
`[${slug}] ${path.relative(repoRoot, registryFile)} must call definePreviewStateRegistry().`,
|
|
108
|
+
);
|
|
109
|
+
failed = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const file of canvasConfigFiles) {
|
|
113
|
+
if (!fileUsesPreviewStateRegistry(file, registryBasename)) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[${slug}] ${path.relative(repoRoot, file)} must import preview states from ${registryBasename}.`,
|
|
116
|
+
);
|
|
117
|
+
failed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const file of stateSelectFiles) {
|
|
122
|
+
if (!fileUsesPreviewStateRegistry(file, registryBasename)) {
|
|
123
|
+
console.error(
|
|
124
|
+
`[${slug}] ${path.relative(repoRoot, file)} must import preview states from ${registryBasename}.`,
|
|
125
|
+
);
|
|
126
|
+
failed = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (failed) {
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log("Prototype preview state verification passed.");
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cn } from "@prototype/lib/utils";
|
|
2
|
+
|
|
3
|
+
type CodeBlockProps = {
|
|
4
|
+
children: string;
|
|
5
|
+
compact?: boolean;
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function CodeBlock({ children, compact = false, className }: CodeBlockProps) {
|
|
10
|
+
return (
|
|
11
|
+
<pre
|
|
12
|
+
className={cn(
|
|
13
|
+
"overflow-auto whitespace-pre-wrap rounded-md bg-muted font-mono text-muted-foreground",
|
|
14
|
+
compact ? "max-h-48 p-2 text-[13px] leading-relaxed" : "max-h-64 p-3 text-sm leading-relaxed",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</pre>
|
|
20
|
+
);
|
|
21
|
+
}
|