Compare commits

..

5 Commits

Author SHA1 Message Date
hypercross 6fcd879287 fix: icon sizing 2026-03-13 12:40:30 +08:00
hypercross 29d8a76cd1 refactor: hide controls for hidden layers 2026-03-13 12:34:39 +08:00
hypercross 97923c8d35 fix: icons 2026-03-13 11:53:41 +08:00
hypercross 9e8d4e6388 feat: md icon prefix 2026-03-13 11:33:13 +08:00
hypercross 346b97153f fix: bug 2026-03-13 10:59:24 +08:00
7 changed files with 87 additions and 51 deletions

View File

@ -1,9 +1,10 @@
import {createMemo, For} from 'solid-js'; import {createMemo, For} from 'solid-js';
import { marked } from '../../markdown'; import {parseMarkdown} from '../../markdown';
import { getLayerStyle } from './hooks/dimensions'; import { getLayerStyle } from './hooks/dimensions';
import type { CardData } from './types'; import type { CardData } from './types';
import {DeckStore} from "./hooks/deckStore"; import {DeckStore} from "./hooks/deckStore";
import {processVariables} from "../utils/csv-loader"; import {processVariables} from "../utils/csv-loader";
import {resolvePath} from "../utils/path";
export interface CardLayerProps { export interface CardLayerProps {
cardData: CardData; cardData: CardData;
@ -16,7 +17,8 @@ export function CardLayer(props: CardLayerProps) {
const showBounds = () => props.store.state.isEditing; const showBounds = () => props.store.state.isEditing;
function renderLayerContent(content: string) { function renderLayerContent(content: string) {
return marked.parse(processVariables(content, props.cardData, props.store.state.cards)) as string; const iconPath = resolvePath(props.store.state.cards.sourcePath, props.cardData.iconPath);
return parseMarkdown(processVariables(content, props.cardData, props.store.state.cards), iconPath) as string;
} }
return ( return (
<For each={layers()}> <For each={layers()}>

View File

@ -49,44 +49,48 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
/> />
<span class="text-sm flex-1">{layer.prop}</span> <span class="text-sm flex-1">{layer.prop}</span>
</div> </div>
<button {layer.visible && (
<>
<button
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)} onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
class={`text-xs px-2 py-1 rounded cursor-pointer ${ class={`text-xs px-2 py-1 rounded cursor-pointer ${
store.state.editingLayer === layer.prop store.state.editingLayer === layer.prop
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`} }`}
> >
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'} {store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
</button> </button>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<select <select
value={layer.orientation || 'n'} value={layer.orientation || 'n'}
onChange={(e) => updateLayerOrientation(layer.prop, e.target.value as 'n' | 's' | 'e' | 'w')} onChange={(e) => updateLayerOrientation(layer.prop, e.target.value as 'n' | 's' | 'e' | 'w')}
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer" class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer"
> >
<For each={ORIENTATION_OPTIONS}> <For each={ORIENTATION_OPTIONS}>
{(opt) => ( {(opt) => (
<option value={opt.value}>{opt.label}</option> <option value={opt.value}>{opt.label}</option>
)} )}
</For> </For>
</select> </select>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label class="text-xs text-gray-600">/mm</label> <label class="text-xs text-gray-600">/mm</label>
<input <input
type="number" type="number"
value={layer.fontSize || ''} value={layer.fontSize || ''}
placeholder="默认" placeholder="默认"
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
updateLayerFontSize(layer.prop, value ? Number(value) : undefined); updateLayerFontSize(layer.prop, value ? Number(value) : undefined);
}} }}
class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white" class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white"
step="0.1" step="0.1"
min="0.1" min="0.1"
/> />
</div> </div>
</>
)}
</div> </div>
)} )}
</For> </For>

View File

@ -74,7 +74,7 @@ export interface DeckActions {
setPadding: (padding: number) => void; setPadding: (padding: number) => void;
// 数据设置 // 数据设置
setCards: (cards: CardData[]) => void; setCards: (cards: CSV<CardData>) => void;
setActiveTab: (index: number) => void; setActiveTab: (index: number) => void;
updateCardData: (index: number, key: string, value: string) => void; updateCardData: (index: number, key: string, value: string) => void;
@ -139,7 +139,7 @@ export function createDeckStore(
src: initialSrc, src: initialSrc,
rawSrc: initialSrc, rawSrc: initialSrc,
dimensions: null, dimensions: null,
cards: [], cards: [] as any,
activeTab: 0, activeTab: 0,
layerConfigs: [], layerConfigs: [],
isEditing: false, isEditing: false,
@ -195,7 +195,7 @@ export function createDeckStore(
updateDimensions(); updateDimensions();
}; };
const setCards = (cards: CardData[]) => setState({ cards, activeTab: 0 }); const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
const setActiveTab = (index: number) => setState({ activeTab: index }); const setActiveTab = (index: number) => setState({ activeTab: index });
const updateCardData = (index: number, key: string, value: string) => { const updateCardData = (index: number, key: string, value: string) => {
setState('cards', index, key, value); setState('cards', index, key, value);
@ -269,7 +269,22 @@ export function createDeckStore(
const generateCode = () => { const generateCode = () => {
const layersStr = formatLayers(state.layerConfigs); const layersStr = formatLayers(state.layerConfigs);
return `:md-deck[${state.rawSrc || state.src}]{size="${state.sizeW}x${state.sizeH}" grid="${state.gridW}x${state.gridH}" bleed="${state.bleed}" padding="${state.padding}" layers="${layersStr}"}`; const parts = [
`:md-deck[${state.rawSrc || state.src}]`,
`{size="${state.sizeW}x${state.sizeH} "`,
`grid="${state.gridW}x${state.gridH} "`
];
// 仅在非默认值时添加 bleed 和 padding
if (state.bleed !== DECK_DEFAULTS.BLEED) {
parts.push(`bleed="${state.bleed} "`);
}
if (state.padding !== DECK_DEFAULTS.PADDING) {
parts.push(`padding="${state.padding} "`);
}
parts.push(`layers="${layersStr}"}`);
return parts.join('');
}; };
const copyCode = async () => { const copyCode = async () => {

View File

@ -57,9 +57,6 @@ export function initLayerConfigs(
): LayerConfig[] { ): LayerConfig[] {
const parsed = parseLayers(existingLayersStr); const parsed = parseLayers(existingLayersStr);
const allProps = Object.keys(data[0] || {}).filter(k => k !== 'label'); const allProps = Object.keys(data[0] || {}).filter(k => k !== 'label');
if(data.frontmatter){
allProps.push(...Object.keys(data.frontmatter));
}
return allProps.map(prop => { return allProps.map(prop => {
const existing = parsed.find(l => l.prop === prop); const existing = parsed.find(l => l.prop === prop);

View File

@ -68,9 +68,10 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
if (frontmatter) { if (frontmatter) {
csvResult.frontmatter = frontmatter; csvResult.frontmatter = frontmatter;
for(const each of result){ for(const each of result){
Object.setPrototypeOf(each, frontmatter); Object.assign(each, frontmatter);
} }
} }
csvResult.sourcePath = path;
csvCache.set(path, result); csvCache.set(path, result);
return csvResult; return csvResult;
@ -82,6 +83,7 @@ interface JSONObject extends Record<string, JSONData> {}
export type CSV<T> = T[] & { export type CSV<T> = T[] & {
frontmatter?: JSONObject; frontmatter?: JSONObject;
sourcePath: string;
} }
export function processVariables<T extends JSONObject> (body: string, currentRow: T, csv: CSV<T>, filtered?: T[], remix?: boolean): string { export function processVariables<T extends JSONObject> (body: string, currentRow: T, csv: CSV<T>, filtered?: T[], remix?: boolean): string {
@ -97,5 +99,5 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
return `{{${key}}}`; return `{{${key}}}`;
} }
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`); return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
} }

View File

@ -5,6 +5,15 @@ import markedAlert from "marked-alert";
import markedMermaid from "./mermaid"; import markedMermaid from "./mermaid";
import {gfmHeadingId} from "marked-gfm-heading-id"; import {gfmHeadingId} from "marked-gfm-heading-id";
let globalIconPrefix: string | undefined = undefined;
function overrideIconPrefix(path?: string){
globalIconPrefix = path;
return {
[Symbol.dispose](){
globalIconPrefix = undefined;
}
}
}
// 使用 marked-directive 来支持指令语法 // 使用 marked-directive 来支持指令语法
const marked = new Marked() const marked = new Marked()
.use(gfmHeadingId()) .use(gfmHeadingId())
@ -26,7 +35,8 @@ const marked = new Marked()
// :[blah] becomes <i class="icon icon-blah"></i> // :[blah] becomes <i class="icon icon-blah"></i>
renderer(token) { renderer(token) {
if (!token.meta.name) { if (!token.meta.name) {
return `<icon class="icon-${token.text}"></icon>`; const style = globalIconPrefix ? `style="--icon-src: url('${globalIconPrefix}/${token.text}.png')"` : '';
return `<icon ${style} class="icon-${token.text}"></icon>`;
} }
return false; return false;
} }
@ -89,8 +99,9 @@ const marked = new Marked()
}] }]
}); });
export function parseMarkdown(content: string): string { export function parseMarkdown(content: string, iconPrefix?: string): string {
return marked.parse(content.trimStart()) as string; using prefix = overrideIconPrefix(iconPrefix);
return marked.parse(content.trimStart()) as string;
} }
export { marked }; export { marked };

View File

@ -2,16 +2,21 @@
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
/* icon */ /* icon */
icon{ icon, pull{
@apply inline-block; @apply inline-block;
width: 1em; width: 1em;
height: 1em; height: 1.27em;
vertical-align: text-bottom;
--icon-src: ''; --icon-src: '';
background: var(--icon-src); background-image: var(--icon-src);
background-size: contain; background-size: contain;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
pull{
margin-right: -.5em;
width: 0;
}
/* prose */ /* prose */