Compare commits
23
Commits
b81613e598
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec68773179 | ||
|
|
d0a1a81ca2 | ||
|
|
39abc86e6e | ||
|
|
7d45dd8073 | ||
|
|
a65536e31e | ||
|
|
785a089518 | ||
|
|
6f101661c7 | ||
|
|
365af027b8 | ||
|
|
4f8d46a523 | ||
|
|
d53f8f4585 | ||
|
|
e028ecc9c8 | ||
|
|
02a17f5283 | ||
|
|
1de77c904e | ||
|
|
cf0f53d148 | ||
|
|
daffe4daca | ||
|
|
8c505a205c | ||
|
|
1bd885f5b0 | ||
|
|
a5d4850728 | ||
|
|
552ed5e6fa | ||
|
|
5a312f5039 | ||
|
|
b890ce4035 | ||
|
|
c999334671 | ||
|
|
193f55fe30 |
@@ -40,3 +40,4 @@ yarn-error.log*
|
|||||||
/outputs/
|
/outputs/
|
||||||
/work/
|
/work/
|
||||||
/mard-source.html
|
/mard-source.html
|
||||||
|
/data/
|
||||||
|
|||||||
+84
-12
@@ -48,13 +48,14 @@ button { color: inherit; }
|
|||||||
.upload-card { width: 100%; min-height: 100px; max-height: 260px; display: block; position: relative; overflow: hidden; border: 1px dashed #a5a59e; background: #ebe7dd; }
|
.upload-card { width: 100%; min-height: 100px; max-height: 260px; display: block; position: relative; overflow: hidden; border: 1px dashed #a5a59e; background: #ebe7dd; }
|
||||||
.upload-card canvas { width: 100%; height: 100%; display: block; pointer-events: none; }
|
.upload-card canvas { width: 100%; height: 100%; display: block; pointer-events: none; }
|
||||||
.upload-card .file-input { display: none; }
|
.upload-card .file-input { display: none; }
|
||||||
.crop-preview { cursor: grab; touch-action: none; }
|
.crop-preview { cursor: default; }
|
||||||
.crop-preview:active { cursor: grabbing; }
|
|
||||||
.crop-frame { position: absolute; inset: 0; border: 2px solid rgba(255,255,255,.85); box-shadow: inset 0 0 0 1px rgba(20,30,25,.22); pointer-events: none; }
|
|
||||||
.upload-overlay { position: absolute; z-index: 3; right: 10px; bottom: 10px; padding: 7px 10px; border: 0; color: white; background: rgba(22, 75, 57, .9); cursor: pointer; font-size: 12px; }
|
.upload-overlay { position: absolute; z-index: 3; right: 10px; bottom: 10px; padding: 7px 10px; border: 0; color: white; background: rgba(22, 75, 57, .9); cursor: pointer; font-size: 12px; }
|
||||||
.file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 9px 0 24px; color: var(--muted); font-size: 11px; }
|
.file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 9px 0 24px; color: var(--muted); font-size: 11px; }
|
||||||
.crop-zoom { margin-top: 0; }
|
.crop-select-button { width: 100%; margin-top: 0; padding: 10px; border: 1px solid var(--ink); background: var(--lime); cursor: pointer; font-size: 12px; font-weight: 700; }
|
||||||
.reset-crop { width: 100%; margin: 7px 0 20px; padding: 7px; border: 1px solid var(--line); background: white; cursor: pointer; color: var(--muted); font-size: 11px; }
|
.reset-crop { width: 100%; margin: 7px 0 0; padding: 7px; border: 1px solid var(--line); background: white; cursor: pointer; color: var(--muted); font-size: 11px; }
|
||||||
|
.pattern-import-button { width: 100%; margin: 7px 0 20px; padding: 9px; border: 1px solid #b6532d; background: #fff5ec; color: #8f3f20; cursor: pointer; font-size: 12px; font-weight: 700; }
|
||||||
|
.pattern-import-button:hover { background: #fee5d1; }
|
||||||
|
.pattern-import-file { display: none; }
|
||||||
.field-row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 8px; }
|
.field-row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 8px; }
|
||||||
.field-row label, .field { display: flex; flex-direction: column; gap: 8px; }
|
.field-row label, .field { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.field-row label span, .field > span { color: var(--muted); font-size: 12px; }
|
.field-row label span, .field > span { color: var(--muted); font-size: 12px; }
|
||||||
@@ -76,9 +77,10 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
.zoom-control button { width: 36px; height: 36px; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 20px; }
|
.zoom-control button { width: 36px; height: 36px; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 20px; }
|
||||||
.zoom-control input { width: 95px; }
|
.zoom-control input { width: 95px; }
|
||||||
.zoom-control output { width: 38px; color: var(--muted); font-size: 11px; text-align: right; }
|
.zoom-control output { width: 38px; color: var(--muted); font-size: 11px; text-align: right; }
|
||||||
.canvas-stage { min-height: 610px; flex: 1; overflow: auto; display: block; padding: 36px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; }
|
.canvas-viewport { position: relative; width: 100%; aspect-ratio: 1 / 1; height: auto; min-height: 0; flex: 0 0 auto; align-self: stretch; contain: size layout; overflow: hidden; }
|
||||||
|
.canvas-stage { position: absolute; inset: 0; width: auto; height: auto; overflow: scroll; display: block; padding: 12px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; scrollbar-gutter: stable; }
|
||||||
.canvas-stage.is-dragging { cursor: grabbing; }
|
.canvas-stage.is-dragging { cursor: grabbing; }
|
||||||
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; }
|
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); background-color: #f7f5ef; background-image: linear-gradient(45deg, #dedbd3 25%, transparent 25%), linear-gradient(-45deg, #dedbd3 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #dedbd3 75%), linear-gradient(-45deg, transparent 75%, #dedbd3 75%); background-position: 0 0, 0 5px, 5px -5px, -5px 0; background-size: 10px 10px; }
|
||||||
.canvas-wrap canvas { cursor: inherit; image-rendering: pixelated; }
|
.canvas-wrap canvas { cursor: inherit; image-rendering: pixelated; }
|
||||||
.pixel-tooltip { position: absolute; z-index: 5; width: max-content; max-width: 260px; padding: 9px 11px; display: grid; grid-template-columns: 14px auto; align-items: center; gap: 3px 8px; color: white; background: rgba(22, 30, 27, .94); border: 1px solid rgba(255,255,255,.2); box-shadow: 4px 4px 0 rgba(0,0,0,.2); border-radius: 3px; line-height: 1.35; pointer-events: none; }
|
.pixel-tooltip { position: absolute; z-index: 5; width: max-content; max-width: 260px; padding: 9px 11px; display: grid; grid-template-columns: 14px auto; align-items: center; gap: 3px 8px; color: white; background: rgba(22, 30, 27, .94); border: 1px solid rgba(255,255,255,.2); box-shadow: 4px 4px 0 rgba(0,0,0,.2); border-radius: 3px; line-height: 1.35; pointer-events: none; }
|
||||||
.tooltip-swatch { width: 14px; height: 14px; grid-row: 1 / 3; border-radius: 50%; border: 1px solid rgba(255,255,255,.6); }
|
.tooltip-swatch { width: 14px; height: 14px; grid-row: 1 / 3; border-radius: 50%; border: 1px solid rgba(255,255,255,.6); }
|
||||||
@@ -129,9 +131,9 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
.history-heading p:not(.section-kicker) { margin: 8px 0 0; color: var(--muted); font-size: 12px; }
|
.history-heading p:not(.section-kicker) { margin: 8px 0 0; color: var(--muted); font-size: 12px; }
|
||||||
.history-heading .search-box { width: min(330px, 100%); }
|
.history-heading .search-box { width: min(330px, 100%); }
|
||||||
.history-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; padding-top: 18px; }
|
.history-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; padding-top: 18px; }
|
||||||
.history-card { min-width: 0; padding: 16px; display: grid; grid-template-columns: 58px minmax(0,1fr); gap: 12px; border: 1px solid var(--line); background: var(--card); }
|
.history-card { min-width: 0; padding: 16px; display: grid; grid-template-columns: 86px minmax(0,1fr); gap: 13px; border: 1px solid var(--line); background: var(--card); }
|
||||||
.history-swatches { width: 58px; height: 58px; display: flex; overflow: hidden; border: 1px solid var(--ink); }
|
.history-thumbnail { width: 86px; height: 86px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--ink); background-color: #f8f6f0; background-image: linear-gradient(45deg, #dedbd3 25%, transparent 25%), linear-gradient(-45deg, #dedbd3 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #dedbd3 75%), linear-gradient(-45deg, transparent 75%, #dedbd3 75%); background-position: 0 0, 0 5px, 5px -5px, -5px 0; background-size: 10px 10px; }
|
||||||
.history-swatches i { min-width: 3px; }
|
.history-thumbnail canvas { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; image-rendering: pixelated; }
|
||||||
.history-card h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 3px 0 7px; font-size: 14px; }
|
.history-card h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 3px 0 7px; font-size: 14px; }
|
||||||
.history-card p { margin: 0; color: var(--muted); font-size: 10px; }
|
.history-card p { margin: 0; color: var(--muted); font-size: 10px; }
|
||||||
.history-actions { grid-column: 1 / -1; display: flex; gap: 7px; justify-content: flex-end; }
|
.history-actions { grid-column: 1 / -1; display: flex; gap: 7px; justify-content: flex-end; }
|
||||||
@@ -139,6 +141,55 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
.history-actions button:first-child { background: var(--lime); font-weight: 700; }
|
.history-actions button:first-child { background: var(--lime); font-weight: 700; }
|
||||||
.history-empty { margin-top: 18px; padding: 44px 20px; color: var(--muted); text-align: center; border: 1px dashed var(--line); background: rgba(255,253,248,.5); font-size: 13px; }
|
.history-empty { margin-top: 18px; padding: 44px 20px; color: var(--muted); text-align: center; border: 1px dashed var(--line); background: rgba(255,253,248,.5); font-size: 13px; }
|
||||||
|
|
||||||
|
.crop-dialog-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 24px; background: rgba(17,24,21,.76); backdrop-filter: blur(5px); }
|
||||||
|
.crop-dialog { width: min(1280px, 96vw); max-height: 94vh; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--ink); background: var(--card); box-shadow: 10px 10px 0 rgba(0,0,0,.32); }
|
||||||
|
.crop-dialog-header { flex: 0 0 auto; padding: 18px 22px; display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line); }
|
||||||
|
.crop-dialog-header .section-kicker { margin-bottom: 5px; }
|
||||||
|
.crop-dialog-header h2 { margin: 0; font-size: 22px; }
|
||||||
|
.crop-dialog-header p:not(.section-kicker) { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
|
||||||
|
.crop-dialog-header > button { width: 38px; height: 38px; flex: 0 0 auto; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 25px; line-height: 1; }
|
||||||
|
.crop-mode-bar { min-height: 54px; padding: 8px 20px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); background: #f7f4ec; color: var(--muted); font-size: 11px; }
|
||||||
|
.crop-mode-switch { display: inline-flex; border: 1px solid var(--ink); background: white; }
|
||||||
|
.crop-mode-switch button { min-height: 34px; padding: 0 15px; border: 0; border-right: 1px solid var(--ink); background: transparent; cursor: pointer; font-size: 12px; }
|
||||||
|
.crop-mode-switch button:last-child { border-right: 0; }
|
||||||
|
.crop-mode-switch button.active { background: var(--green); color: white; font-weight: 700; }
|
||||||
|
.pattern-import-dimensions { display: flex; align-items: flex-end; gap: 8px; }
|
||||||
|
.pattern-import-dimensions label { display: grid; gap: 3px; color: var(--muted); font-size: 10px; }
|
||||||
|
.pattern-import-dimensions input { width: 82px; height: 34px; padding: 0 8px; border: 1px solid var(--line); background: white; color: var(--ink); font-size: 12px; }
|
||||||
|
.pattern-import-dimensions b { padding-bottom: 9px; color: var(--muted); font-weight: 400; }
|
||||||
|
.pattern-import-empty-option { display: flex; align-items: center; gap: 6px; color: var(--ink); cursor: pointer; }
|
||||||
|
.pattern-import-empty-option input { accent-color: var(--green); }
|
||||||
|
.crop-size-readout { margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||||
|
.crop-dialog-scroll { min-height: 0; flex: 1 1 auto; overflow: auto; padding: 18px; display: grid; place-items: center; background-color: #e9e5db; background-image: radial-gradient(#c8c3b8 .7px, transparent .7px); background-size: 12px 12px; }
|
||||||
|
.crop-dialog-stage { position: relative; width: max-content; max-width: 100%; line-height: 0; cursor: crosshair; touch-action: none; user-select: none; box-shadow: 0 0 0 1px var(--ink); }
|
||||||
|
.crop-dialog-stage.can-move-selection { cursor: move; }
|
||||||
|
.crop-dialog-stage.is-moving-selection { cursor: grabbing; }
|
||||||
|
.crop-dialog-stage.is-resizing-selection { cursor: nwse-resize; }
|
||||||
|
.crop-dialog-stage canvas { display: block; max-width: 100%; height: auto; pointer-events: none; }
|
||||||
|
.crop-selection-mask { position: absolute; z-index: 1; background: rgba(10,15,13,.55); pointer-events: none; }
|
||||||
|
.crop-selection-top { inset: 0 0 auto; }
|
||||||
|
.crop-selection-left { left: 0; }
|
||||||
|
.crop-selection-bottom { inset: auto 0 0; bottom: 0; }
|
||||||
|
.crop-selection-box { position: absolute; z-index: 2; border: 2px solid white; outline: 1px solid rgba(0,0,0,.75); box-shadow: inset 0 0 0 1px rgba(0,0,0,.18); pointer-events: none; }
|
||||||
|
.crop-selection-box::before, .crop-selection-box::after { content: ""; position: absolute; background: rgba(255,255,255,.62); }
|
||||||
|
.crop-selection-box::before { left: 33.333%; top: 0; bottom: 0; width: 1px; box-shadow: calc(33.333% * 1) 0 0 rgba(255,255,255,.62); }
|
||||||
|
.crop-selection-box::after { left: 0; right: 0; top: 33.333%; height: 1px; box-shadow: 0 calc(33.333% * 1) 0 rgba(255,255,255,.62); }
|
||||||
|
.pattern-import-grid-overlay { position: absolute; z-index: 3; inset: 0; display: block; pointer-events: none; background-image: linear-gradient(to right, rgba(255,102,0,.66) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,102,0,.66) 1px, transparent 1px); box-shadow: inset -1px -1px 0 rgba(255,102,0,.66); }
|
||||||
|
.crop-resize-handle { position: absolute; z-index: 4; width: 15px; height: 15px; display: block; border: 2px solid white; border-radius: 50%; background: var(--green); box-shadow: 0 1px 4px rgba(0,0,0,.48); pointer-events: auto; }
|
||||||
|
.handle-nw { left: 0; top: 0; transform: translate(-50%, -50%); cursor: nwse-resize; }
|
||||||
|
.handle-ne { right: 0; top: 0; transform: translate(50%, -50%); cursor: nesw-resize; }
|
||||||
|
.handle-sw { left: 0; bottom: 0; transform: translate(-50%, 50%); cursor: nesw-resize; }
|
||||||
|
.handle-se { right: 0; bottom: 0; transform: translate(50%, 50%); cursor: nwse-resize; }
|
||||||
|
.handle-n { left: 50%; top: 0; transform: translate(-50%, -50%); cursor: ns-resize; }
|
||||||
|
.handle-s { left: 50%; bottom: 0; transform: translate(-50%, 50%); cursor: ns-resize; }
|
||||||
|
.handle-w { left: 0; top: 50%; transform: translate(-50%, -50%); cursor: ew-resize; }
|
||||||
|
.handle-e { right: 0; top: 50%; transform: translate(50%, -50%); cursor: ew-resize; }
|
||||||
|
.crop-dialog-actions { flex: 0 0 auto; min-height: 66px; padding: 12px 20px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; border-top: 1px solid var(--line); }
|
||||||
|
.crop-dialog-actions span { margin-right: auto; color: var(--muted); font-size: 11px; }
|
||||||
|
.crop-dialog-actions button { min-height: 40px; padding: 0 16px; border: 1px solid var(--ink); background: white; cursor: pointer; font-size: 12px; }
|
||||||
|
.crop-dialog-actions .confirm { background: var(--green); color: white; font-weight: 700; }
|
||||||
|
.crop-dialog-actions .confirm:disabled { opacity: .42; cursor: not-allowed; }
|
||||||
|
|
||||||
@media (max-width: 1160px) {
|
@media (max-width: 1160px) {
|
||||||
.workspace { grid-template-columns: 260px minmax(500px, 1fr); }
|
.workspace { grid-template-columns: 260px minmax(500px, 1fr); }
|
||||||
.color-panel { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--ink); max-height: none; }
|
.color-panel { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--ink); max-height: none; }
|
||||||
@@ -160,7 +211,8 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
.pattern-toolbar { align-items: flex-start; flex-direction: column; padding: 20px; }
|
.pattern-toolbar { align-items: flex-start; flex-direction: column; padding: 20px; }
|
||||||
.zoom-control { width: 100%; }
|
.zoom-control { width: 100%; }
|
||||||
.zoom-control input { flex: 1; }
|
.zoom-control input { flex: 1; }
|
||||||
.canvas-stage { min-height: 440px; display: block; padding: 24px; }
|
.canvas-viewport { width: 100%; height: auto; min-height: 0; aspect-ratio: 1 / 1; flex-basis: auto; }
|
||||||
|
.canvas-stage { display: block; padding: 10px; }
|
||||||
.canvas-wrap { margin: auto; }
|
.canvas-wrap { margin: auto; }
|
||||||
.pattern-footer { align-items: flex-start; flex-wrap: wrap; padding: 14px 18px; }
|
.pattern-footer { align-items: flex-start; flex-wrap: wrap; padding: 14px 18px; }
|
||||||
.view-options { width: 100%; margin: 5px 0 0; justify-content: space-between; }
|
.view-options { width: 100%; margin: 5px 0 0; justify-content: space-between; }
|
||||||
@@ -173,6 +225,26 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
.history-section { margin: 52px auto; }
|
.history-section { margin: 52px auto; }
|
||||||
.history-heading .search-box { width: 100%; }
|
.history-heading .search-box { width: 100%; }
|
||||||
.history-grid { grid-template-columns: 1fr; }
|
.history-grid { grid-template-columns: 1fr; }
|
||||||
|
.crop-dialog-backdrop { padding: 0; }
|
||||||
|
.crop-dialog { width: 100vw; max-height: 100vh; height: 100vh; border: 0; box-shadow: none; }
|
||||||
|
.crop-dialog-header { padding: 14px 16px; }
|
||||||
|
.crop-dialog-header h2 { font-size: 18px; }
|
||||||
|
.crop-mode-bar { padding: 8px 12px; flex-wrap: wrap; gap: 7px 10px; }
|
||||||
|
.crop-mode-switch { flex: 1; }
|
||||||
|
.crop-mode-switch button { flex: 1; padding: 0 8px; }
|
||||||
|
.pattern-import-dimensions { width: 100%; }
|
||||||
|
.pattern-import-dimensions label { flex: 1; }
|
||||||
|
.pattern-import-dimensions input { width: 100%; min-height: 42px; }
|
||||||
|
.pattern-import-empty-option { width: 100%; min-height: 34px; }
|
||||||
|
.crop-size-readout { width: 100%; margin-left: 0; }
|
||||||
|
.crop-resize-handle { width: 28px; height: 28px; border-width: 3px; }
|
||||||
|
.crop-edge-handle { display: none; }
|
||||||
|
.crop-dialog-scroll { padding: 10px; }
|
||||||
|
.crop-dialog-stage { width: 100%; max-width: 100%; }
|
||||||
|
.crop-dialog-stage canvas { width: 100%; max-width: 100%; height: auto; }
|
||||||
|
.crop-dialog-actions { padding: 10px 12px; flex-wrap: wrap; }
|
||||||
|
.crop-dialog-actions span { width: 100%; order: -1; }
|
||||||
|
.crop-dialog-actions button { flex: 1; padding: 0 10px; }
|
||||||
input, select, button { font-size: 16px; }
|
input, select, button { font-size: 16px; }
|
||||||
.search-box input, .color-item, .selection-tools button { font-size: 12px; }
|
.search-box input, .color-item, .selection-tools button { font-size: 12px; }
|
||||||
}
|
}
|
||||||
@@ -180,7 +252,7 @@ input[type="range"] { accent-color: var(--green); }
|
|||||||
@media (max-width: 370px) {
|
@media (max-width: 370px) {
|
||||||
.view-options { gap: 6px; }
|
.view-options { gap: 6px; }
|
||||||
.view-options label { font-size: 10px; }
|
.view-options label { font-size: 10px; }
|
||||||
.canvas-stage { padding: 16px; }
|
.canvas-stage { padding: 8px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; transition: none !important; } }
|
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; transition: none !important; } }
|
||||||
|
|||||||
+1382
-149
File diff suppressed because it is too large
Load Diff
+66
-1
@@ -1,10 +1,22 @@
|
|||||||
import { createReadStream, existsSync, statSync } from "node:fs";
|
import { createReadStream, existsSync, mkdirSync, statSync } from "node:fs";
|
||||||
|
import { appendFile } from "node:fs/promises";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { extname, join, normalize } from "node:path";
|
import { extname, join, normalize } from "node:path";
|
||||||
|
|
||||||
const packagedRoot = join(process.cwd(), "public");
|
const packagedRoot = join(process.cwd(), "public");
|
||||||
const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out");
|
const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out");
|
||||||
const port = Number(process.env.APP_PORT || 3200);
|
const port = Number(process.env.APP_PORT || 3200);
|
||||||
|
const dataDirectory = process.env.DATA_DIR || join(process.cwd(), "data");
|
||||||
|
const usageLogPath = join(dataDirectory, "usage.jsonl");
|
||||||
|
mkdirSync(dataDirectory, { recursive: true });
|
||||||
|
|
||||||
|
function formatChinaTimestamp(date) {
|
||||||
|
return new Date(date.getTime() + 8 * 60 * 60 * 1000)
|
||||||
|
.toISOString()
|
||||||
|
.replace("Z", "+08:00");
|
||||||
|
}
|
||||||
|
|
||||||
const mimeTypes = {
|
const mimeTypes = {
|
||||||
".css": "text/css; charset=utf-8",
|
".css": "text/css; charset=utf-8",
|
||||||
".html": "text/html; charset=utf-8",
|
".html": "text/html; charset=utf-8",
|
||||||
@@ -17,8 +29,61 @@ const mimeTypes = {
|
|||||||
".webp": "image/webp",
|
".webp": "image/webp",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function sendJson(response, status, value) {
|
||||||
|
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
||||||
|
response.end(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUsageLog(request, response) {
|
||||||
|
let body = "";
|
||||||
|
request.setEncoding("utf8");
|
||||||
|
request.on("data", (chunk) => {
|
||||||
|
body += chunk;
|
||||||
|
if (body.length > 16_384) request.destroy();
|
||||||
|
});
|
||||||
|
request.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(body || "{}");
|
||||||
|
if (!['page_view', 'conversion'].includes(input.event)) {
|
||||||
|
sendJson(response, 400, { error: "Invalid event" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const entry = {
|
||||||
|
timestamp: now.toISOString(),
|
||||||
|
timestamp_cn: formatChinaTimestamp(now),
|
||||||
|
event: input.event,
|
||||||
|
client_hash: request.headers["x-forwarded-for"] || request.socket.remoteAddress
|
||||||
|
? createHash("sha256").update(String(request.headers["x-forwarded-for"] || request.socket.remoteAddress)).digest("hex").slice(0, 16)
|
||||||
|
: undefined,
|
||||||
|
...(input.event === "conversion" ? {
|
||||||
|
width: Math.max(8, Math.min(256, Number(input.width) || 0)),
|
||||||
|
height: Math.max(8, Math.min(256, Number(input.height) || 0)),
|
||||||
|
sampling_strategy: input.sampling_strategy === "dominant" ? "dominant" : "smooth",
|
||||||
|
color_limit: Math.max(2, Math.min(64, Number(input.color_limit) || 0)),
|
||||||
|
actual_colors: Math.max(0, Math.min(221, Number(input.actual_colors) || 0)),
|
||||||
|
bead_count: Math.max(0, Math.min(65_536, Number(input.bead_count) || 0)),
|
||||||
|
transparent_cells: Math.max(0, Math.min(65_536, Number(input.transparent_cells) || 0)),
|
||||||
|
} : {}),
|
||||||
|
};
|
||||||
|
await appendFile(usageLogPath, `${JSON.stringify(entry)}\n`, "utf8");
|
||||||
|
sendJson(response, 202, { ok: true });
|
||||||
|
} catch {
|
||||||
|
sendJson(response, 400, { error: "Invalid request" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createServer((request, response) => {
|
createServer((request, response) => {
|
||||||
const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname);
|
const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname);
|
||||||
|
if (pathname === "/api/usage" && request.method === "POST") {
|
||||||
|
writeUsageLog(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname.startsWith("/api/")) {
|
||||||
|
sendJson(response, 404, { error: "Not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const relativePath = normalize(pathname).replace(/^([/\\])+/, "");
|
const relativePath = normalize(pathname).replace(/^([/\\])+/, "");
|
||||||
let filePath = join(root, relativePath || "index.html");
|
let filePath = join(root, relativePath || "index.html");
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-3200}:3200"
|
- "${APP_PORT:-3200}:3200"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"name": "rolldown-runtime"
|
"name": "rolldown-runtime"
|
||||||
},
|
},
|
||||||
"app/page.tsx": {
|
"app/page.tsx": {
|
||||||
"file": "_next/static/chunks/page-DVje0NqX.js",
|
"file": "_next/static/chunks/page-DBRuBfy6.js",
|
||||||
"name": "page",
|
"name": "page",
|
||||||
"src": "app/page.tsx",
|
"src": "app/page.tsx",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/vinext/dist/shims/layout-segment-context.js": {
|
"node_modules/vinext/dist/shims/layout-segment-context.js": {
|
||||||
"file": "_next/static/chunks/layout-segment-context-CNkt82c6.js",
|
"file": "_next/static/chunks/layout-segment-context-rAphBSzO.js",
|
||||||
"name": "layout-segment-context",
|
"name": "layout-segment-context",
|
||||||
"src": "node_modules/vinext/dist/shims/layout-segment-context.js",
|
"src": "node_modules/vinext/dist/shims/layout-segment-context.js",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"virtual:vinext-app-browser-entry": {
|
"virtual:vinext-app-browser-entry": {
|
||||||
"file": "_next/static/chunks/index-Vssv93iX.js",
|
"file": "_next/static/chunks/index-VFVBI02U.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "virtual:vinext-app-browser-entry",
|
"src": "virtual:vinext-app-browser-entry",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.miWu1gnb.css" data-rsc-css-href="/_next/static/css/index.miWu1gnb.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-Vssv93iX.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-Vssv93iX.js" />
|
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.B4qFKCfT.css" data-rsc-css-href="/_next/static/css/index.B4qFKCfT.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-VFVBI02U.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-VFVBI02U.js" />
|
||||||
</head><body><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-Vssv93iX.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.miWu1gnb.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.miWu1gnb.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.miWu1gnb.css\",\"data-rsc-css-href\":\"/_next/static/css/index.miWu1gnb.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html>
|
</head><body><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-VFVBI02U.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.B4qFKCfT.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.B4qFKCfT.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.B4qFKCfT.css\",\"data-rsc-css-href\":\"/_next/static/css/index.B4qFKCfT.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html>
|
||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-Vssv93iX.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap};
|
import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-VFVBI02U.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
7:I["8c0f216c4604",[],"Slot",1]
|
7:I["8c0f216c4604",[],"Slot",1]
|
||||||
8:I["9276801271d6",[],"AppRouterScrollTarget",1]
|
8:I["9276801271d6",[],"AppRouterScrollTarget",1]
|
||||||
9:I["593f344dc510",[],"RedirectBoundary",1]
|
9:I["593f344dc510",[],"RedirectBoundary",1]
|
||||||
:HL["/_next/static/css/index.miWu1gnb.css","style" ]
|
:HL["/_next/static/css/index.B4qFKCfT.css","style" ]
|
||||||
0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.miWu1gnb.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.miWu1gnb.css","data-rsc-css-href":"/_next/static/css/index.miWu1gnb.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"0c8214d7-bb61-42e2-806c-7003c30369a3","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}}
|
0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.B4qFKCfT.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.B4qFKCfT.css","data-rsc-css-href":"/_next/static/css/index.B4qFKCfT.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"feb6bb1f-6465-425f-8535-1fdc32d89efe","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}}
|
||||||
a:I["6efdf509a785",[],"default",1]
|
a:I["6efdf509a785",[],"default",1]
|
||||||
1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}]
|
1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}]
|
||||||
b:{}
|
b:{}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"appBrowserEntry": "_next/static/chunks/index-Vssv93iX.js"
|
"appBrowserEntry": "_next/static/chunks/index-VFVBI02U.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,51 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { access, readFile, readdir } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
const developmentPreviewMeta =
|
|
||||||
/<meta(?=[^>]*\bname=["']codex-preview["'])(?=[^>]*\bcontent=["']development["'])[^>]*>/i;
|
|
||||||
const templateRoot = new URL("../", import.meta.url);
|
|
||||||
const previewRoot = new URL("../app/_sites-preview/", import.meta.url);
|
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
const workerUrl = new URL("../dist/server/index.js", import.meta.url);
|
const workerUrl = new URL("../dist/server/index.js", import.meta.url);
|
||||||
workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`);
|
workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`);
|
||||||
const { default: worker } = await import(workerUrl.href);
|
const { default: worker } = await import(workerUrl.href);
|
||||||
|
|
||||||
return worker.fetch(
|
return worker.fetch(
|
||||||
new Request("http://localhost/", {
|
new Request("http://localhost/", { headers: { accept: "text/html" } }),
|
||||||
headers: { accept: "text/html" },
|
{ ASSETS: { fetch: async () => new Response("Not found", { status: 404 }) } },
|
||||||
}),
|
{ waitUntil() {}, passThroughOnException() {} },
|
||||||
{
|
|
||||||
ASSETS: {
|
|
||||||
fetch: async () => new Response("Not found", { status: 404 }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
waitUntil() {},
|
|
||||||
passThroughOnException() {},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("server-renders the starter loading skeleton", async () => {
|
test("server-renders the pixel bead application shell", async () => {
|
||||||
const response = await render();
|
const response = await render();
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
|
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
assert.match(html, developmentPreviewMeta);
|
assert.match(html, /<title>豆格工坊|图片转拼豆图纸<\/title>/i);
|
||||||
assert.match(html, /<title>Your site is taking shape<\/title>/i);
|
assert.doesNotMatch(html, /导入现成图纸/);
|
||||||
assert.match(html, /Building your site/);
|
assert.match(html, /MARD 221 基础色卡/);
|
||||||
assert.match(html, /Your site is taking shape/);
|
assert.match(html, /图片只在当前浏览器中处理/);
|
||||||
assert.match(
|
assert.doesNotMatch(html, /codex-preview|Your site is taking shape/i);
|
||||||
html,
|
|
||||||
/Your first version will appear here automatically when it’s ready\./,
|
|
||||||
);
|
|
||||||
assert.doesNotMatch(html, /Codex/);
|
|
||||||
assert.match(html, /react-loading-skeleton/);
|
|
||||||
assert.match(html, /role="status"/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps the loading skeleton scoped and disposable", async () => {
|
test("keeps MARD pattern import wired to the crop and recognition flow", async () => {
|
||||||
const [preview, css, page, layout, packageJson, files] = await Promise.all([
|
const [page, css, palette] = await Promise.all([
|
||||||
readFile(new URL("SkeletonPreview.tsx", previewRoot), "utf8"),
|
|
||||||
readFile(new URL("preview.css", previewRoot), "utf8"),
|
|
||||||
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
readFile(new URL("../app/page.tsx", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../app/layout.tsx", import.meta.url), "utf8"),
|
readFile(new URL("../app/globals.css", import.meta.url), "utf8"),
|
||||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
readFile(new URL("../app/mard-palette.ts", import.meta.url), "utf8"),
|
||||||
readdir(previewRoot),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.deepEqual(files.sort(), ["SkeletonPreview.tsx", "preview.css"]);
|
assert.match(page, /function importMardPatternImage\(/);
|
||||||
assert.match(preview, /from "react-loading-skeleton"/);
|
assert.match(page, /const ENABLE_PATTERN_IMPORT = false/);
|
||||||
assert.match(preview, /baseColor="#eceae7"/);
|
assert.match(page, /recognizedCodes/);
|
||||||
assert.match(preview, /highlightColor="#f9f8f6"/);
|
assert.match(page, /missingCodeAsEmpty/);
|
||||||
assert.match(preview, /duration=\{2\.8\}/);
|
assert.match(page, /没有识别到 MARD 编码的格子视为空白/);
|
||||||
assert.match(preview, /sites-skeleton-search-placeholder/);
|
assert.match(page, /pattern-import-grid-overlay/);
|
||||||
assert.match(packageJson, /"react-loading-skeleton": "3\.5\.0"/);
|
assert.match(page, /max="256"/);
|
||||||
|
assert.match(page, /maximumExportSide = 8192/);
|
||||||
const shellIndex = preview.indexOf('className="sites-skeleton-shell"');
|
assert.match(page, /color\.code.*color\.count.*颗/);
|
||||||
const statusIndex = preview.indexOf('className="sites-skeleton-status"');
|
assert.match(page, /下载高清 PNG/);
|
||||||
assert.ok(shellIndex >= 0 && statusIndex > shellIndex);
|
assert.match(css, /\.pattern-import-grid-overlay/);
|
||||||
assert.match(css, /position:\s*fixed/);
|
assert.match(css, /@media \(max-width: 760px\)/);
|
||||||
assert.match(css, /inset:\s*0/);
|
assert.match(palette, /export const MARD_221/);
|
||||||
assert.match(css, /opacity:\s*0\.52/);
|
assert.match(palette, /A:\s*"#[0-9A-F]{6}/);
|
||||||
assert.match(css, /prefers-reduced-motion:\s*reduce/);
|
|
||||||
assert.doesNotMatch(css, /#020617|canvas|pets|progress/i);
|
|
||||||
assert.doesNotMatch(
|
|
||||||
preview,
|
|
||||||
/loading-spinner|status-mark|status-progress|canvas|cookie|random/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.match(page, /export const metadata:\s*Metadata/);
|
|
||||||
assert.match(page, /"codex-preview": "development"/);
|
|
||||||
assert.match(page, /<SkeletonPreview \/>/);
|
|
||||||
assert.match(layout, /title:\s*"Starter Project"/);
|
|
||||||
assert.doesNotMatch(layout, /codex-preview|_sites-preview|themeColor|\bViewport\b/);
|
|
||||||
assert.doesNotMatch(css, /(^|\s)(html|body)\s*\{/m);
|
|
||||||
|
|
||||||
await assert.rejects(
|
|
||||||
access(new URL("public/_sites-preview", templateRoot)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user