Skip to content

Commit a631ba8

Browse files
author
Marvin Zhang
committed
feat: implement semantic key generation and update UI components for theme support
1 parent 7930982 commit a631ba8

File tree

11 files changed

+258
-157
lines changed

11 files changed

+258
-157
lines changed

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,5 @@
6262
"better-sqlite3": "^11.10.0",
6363
"dotenv": "16.5.0",
6464
"tsx": "^4.0.0"
65-
},
66-
"packageManager": "[email protected]+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
65+
}
6766
}

packages/core/src/services/devlog-service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
import { DevlogDependencyEntity, DevlogEntryEntity, DevlogNoteEntity } from '../entities/index.js';
2020
import { getDataSource } from '../utils/typeorm-config.js';
2121
import { DevlogValidator } from '../validation/devlog-schemas.js';
22+
import { generateDevlogKey } from '../utils/key-generator.js';
2223

2324
interface DevlogServiceInstance {
2425
service: DevlogService;
@@ -107,6 +108,15 @@ export class DevlogService {
107108

108109
const validatedEntry = validation.data;
109110

111+
// Generate a semantic key if not provided
112+
if (!validatedEntry.key) {
113+
validatedEntry.key = generateDevlogKey(
114+
validatedEntry.title,
115+
validatedEntry.type,
116+
validatedEntry.description,
117+
);
118+
}
119+
110120
// If this is an update (entry has ID), validate status transition
111121
if (validatedEntry.id) {
112122
const existingEntity = await this.devlogRepository.findOne({

packages/core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './errors.js';
88
export * from './env-loader.js';
99
export * from './field-change-tracking.js';
1010
export * from './change-history.js';
11+
export * from './key-generator.js';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Semantic key generation utility
3+
* Generates kebab-case keys with hash suffixes to ensure uniqueness
4+
*/
5+
6+
import { createHash } from 'crypto';
7+
8+
/**
9+
* Generate a semantic key from title with hash suffix for uniqueness
10+
*
11+
* @param title - The title to generate a key from
12+
* @param hashInput - Additional input for hash generation (e.g., description, timestamp)
13+
* @returns A semantic key in kebab-case format with hash suffix
14+
*
15+
* @example
16+
* generateSemanticKey("Fix Authentication Bug")
17+
* // Returns: "fix-authentication-bug-a1b2c3d4"
18+
*
19+
* generateSemanticKey("Test Long Title", "some additional context")
20+
* // Returns: "test-long-title-e5f6g7h8"
21+
*/
22+
export function generateSemanticKey(title: string, hashInput?: string): string {
23+
// 1. Create semantic base from title
24+
const semanticBase = title
25+
.toLowerCase()
26+
.replace(/[^a-z0-9\s]/g, '') // Remove special characters
27+
.replace(/\s+/g, '-') // Replace spaces with hyphens
28+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
29+
.substring(0, 40); // Limit base to reasonable length
30+
31+
// 2. Generate hash suffix for uniqueness
32+
const hashSource = hashInput ? `${title}-${hashInput}-${Date.now()}` : `${title}-${Date.now()}`;
33+
34+
const hash = createHash('sha256').update(hashSource).digest('hex').substring(0, 8); // Use first 8 characters
35+
36+
// 3. Combine semantic base with hash suffix
37+
return `${semanticBase}-${hash}`;
38+
}
39+
40+
/**
41+
* Generate a key from a title with additional context for uniqueness
42+
*
43+
* @param title - The main title
44+
* @param type - The entry type (feature, bugfix, etc.)
45+
* @param description - Optional description for additional context
46+
* @returns A semantic key with hash suffix
47+
*/
48+
export function generateDevlogKey(title: string, type: string, description?: string): string {
49+
const contextInput = description ? `${type}-${description}` : type;
50+
51+
return generateSemanticKey(title, contextInput);
52+
}
53+
54+
/**
55+
* Validate that a key meets the required format
56+
*
57+
* @param key - The key to validate
58+
* @returns True if key is valid
59+
*/
60+
export function isValidKey(key: string): boolean {
61+
// Key should be kebab-case with optional hash suffix
62+
const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
63+
return keyPattern.test(key) && key.length <= 255;
64+
}
65+
66+
/**
67+
* Extract the semantic part (without hash) from a generated key
68+
*
69+
* @param key - The generated key
70+
* @returns The semantic part of the key
71+
*/
72+
export function extractSemanticPart(key: string): string {
73+
// Remove the last segment if it looks like a hash (8 hex chars)
74+
const parts = key.split('-');
75+
const lastPart = parts[parts.length - 1];
76+
77+
if (lastPart.length === 8 && /^[a-f0-9]+$/.test(lastPart)) {
78+
return parts.slice(0, -1).join('-');
79+
}
80+
81+
return key;
82+
}

packages/web/app/components/custom/MarkdownEditor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import React, { useEffect, useRef } from 'react';
4+
import { useTheme } from 'next-themes';
45
import CodeEditor from '@uiw/react-textarea-code-editor';
56

67
interface MarkdownEditorProps {
@@ -20,9 +21,13 @@ export function MarkdownEditor({
2021
placeholder,
2122
autoFocus = true,
2223
}: MarkdownEditorProps) {
24+
const { theme, resolvedTheme } = useTheme();
2325
const editorRef = useRef<HTMLTextAreaElement>(null);
2426
const currentValueRef = useRef(value);
2527

28+
// Determine the color mode based on theme
29+
const colorMode = resolvedTheme === 'dark' ? 'dark' : 'light';
30+
2631
// Update current value ref when value changes
2732
useEffect(() => {
2833
currentValueRef.current = value;
@@ -56,7 +61,7 @@ export function MarkdownEditor({
5661
const processedValue = value.replace(/\\n/g, '\n');
5762

5863
return (
59-
<div className="w-full rounded-md overflow-hidden bg-white [&_.w-tc-editor]:!p-4 [&_.w-tc-editor_.w-tc-editor-text]:!p-0 [&_.w-tc-editor_.w-tc-editor-text]:!bg-transparent [&_.w-tc-editor_.w-tc-editor-text]:!text-sm [&_.w-tc-editor_.w-tc-editor-preview]:!p-0 [&_.w-tc-editor_.w-tc-editor-preview]:!text-sm">
64+
<div className="w-full rounded-md overflow-hidden bg-background [&_.w-tc-editor]:!p-4 [&_.w-tc-editor_.w-tc-editor-text]:!p-0 [&_.w-tc-editor_.w-tc-editor-text]:!bg-transparent [&_.w-tc-editor_.w-tc-editor-text]:!text-sm [&_.w-tc-editor_.w-tc-editor-preview]:!p-0 [&_.w-tc-editor_.w-tc-editor-preview]:!text-sm">
6065
<CodeEditor
6166
ref={editorRef}
6267
value={processedValue}
@@ -66,7 +71,7 @@ export function MarkdownEditor({
6671
onBlur={handleBlur}
6772
onKeyDown={handleKeyDown}
6873
padding={12}
69-
data-color-mode="light"
74+
data-color-mode={colorMode}
7075
/>
7176
</div>
7277
);

0 commit comments

Comments
 (0)