Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion helpers/HTMLView.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ export async function getNoteContentAsHTML(content: string, note: TNote): Promis
}
}

// Ensure horizontal rules have a blank line before them (required by showdown)
// Track which HRs already had blank lines for extra padding
// HR patterns: ---, ___, *** (with optional spaces between)
const HR_REGEX = /^(\s*)([-_*])\s*\2\s*\2\s*$/
const HR_MARKER = '<!--HR_WITH_SPACE-->'
for (let i = 1; i < lines.length; i++) {
if (lines[i].match(HR_REGEX)) {
if (lines[i - 1].trim() === '') {
// Already has blank line - mark it for extra padding
lines[i] = `${HR_MARKER}${lines[i]}`
} else {
// Insert blank line before HR if previous line isn't already blank
lines.splice(i, 0, '')
i++ // skip the newly inserted blank line
}
}
}

// Make this proper Markdown -> HTML via showdown library
// Set some options to turn on various more advanced HTML conversions (see actual code at https://github.com/showdownjs/showdown/blob/master/src/options.js#L109):
const converterOptions = {
Expand All @@ -168,7 +186,43 @@ export async function getNoteContentAsHTML(content: string, note: TNote): Promis
}
const converter = new showdown.Converter(converterOptions)
let body = converter.makeHtml(lines.join(`\n`))
body = `<style>img { background: white; max-width: 100%; max-height: 100%; }</style>${body}` // fix for bug in showdown

// Add CSS for proper spacing and layout
const inlineStyles = `<style>
body {
line-height: var(--body-line-height, 1.6);
}
p {
line-height: var(--body-line-height, 1.6);
margin-bottom: 0.8em;
}
/* Add extra spacing after line breaks - creates visual gap between explicit line breaks */
br::after {
content: "";
display: block;
margin-bottom: 0.75em;
}
/* Also add top margin to elements that follow a br tag */
br + * {
margin-top: 0.5em;
}
img {
background: white;
max-width: 100%;
max-height: 100%;
}
hr {
margin-top: 1.5em;
margin-bottom: 1em;
}
hr.with-extra-space {
margin-top: 3em;
}
</style>`
body = inlineStyles + body

// Replace markers for HRs that had blank lines with classed HRs
body = body.replace(/<!--HR_WITH_SPACE--><hr \/>/g, '<hr class="with-extra-space" />')

const imgTagRegex = /<img src=\"(.*?)\"/g
const matches = [...body.matchAll(imgTagRegex)]
Expand Down
75 changes: 35 additions & 40 deletions helpers/NPThemeToCSS.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ---------------------------------------------------------

import { clo, logDebug, logError, logInfo, logWarn, JSP } from '@helpers/dev'
import { hexToRgb, mixHexColors } from '@helpers/colors'

// ---------------------------------------------------------
// Constants and Types
Expand Down Expand Up @@ -131,6 +132,11 @@ export function generateCSSFromTheme(themeNameIn: string = ''): string {
tempSel.push(`font-size: ${baseFontSize}px`)
output.push(makeCSSSelector('body, .body', tempSel))
rootSel.push(`--fg-main-color: ${thisColor}`)
// Also add RGB version for use in rgba() functions
const rgbValues = hexToRgb(thisColor)
if (rgbValues) {
rootSel.push(`--fg-main-color-rgb: ${rgbValues.r}, ${rgbValues.g}, ${rgbValues.b}`)
}
if (styleObj?.lineSpacing) {
// borrowed from convertStyleObjectBlock()
const lineSpacingRem = (Number(styleObj?.lineSpacing) * 1.5).toPrecision(3) // some fudge factor seems to be needed
Expand Down Expand Up @@ -402,6 +408,34 @@ export function generateCSSFromTheme(themeNameIn: string = ''): string {
tempSel = tempSel.concat(convertStyleObjectBlock(styleObj))
output.push(makeCSSSelector('.timeBlock', tempSel))

// Set table styling with subtle borders based on foreground color
tempSel = []
tempSel.push('border-collapse: collapse')
tempSel.push('width: 100%')
tempSel.push('margin: 1rem 0')
output.push(makeCSSSelector('table', tempSel))

tempSel = []
tempSel.push('border: 1px solid rgba(var(--fg-main-color-rgb), 0.2)')
tempSel.push('padding: 0.5rem')
output.push(makeCSSSelector('th, td', tempSel))

tempSel = []
tempSel.push('text-align: left')
tempSel.push('font-weight: 600')
tempSel.push('background-color: rgba(var(--fg-main-color-rgb), 0.05)')
output.push(makeCSSSelector('th', tempSel))

// Set list item spacing for better readability
tempSel = []
tempSel.push('margin-bottom: 0.5rem')
tempSel.push('padding-bottom: 0.25rem')
output.push(makeCSSSelector('li', tempSel))

tempSel = []
tempSel.push('margin-bottom: 0.75rem')
output.push(makeCSSSelector('ul, ol', tempSel))

// Now put the important info and rootSel at the start of the output
output.unshift(makeCSSSelector(':root', rootSel))
output.unshift(`/* Generated from theme '${themeName}' by @jgclark's generateCSSFromTheme */`)
Expand Down Expand Up @@ -627,46 +661,7 @@ export function RGBColourConvert(RGBIn: string): string {
}
}

/**
* Note: in future it should be possible to do this in CSS with `color-mix(in srgb, <color-A>, <color-B>)`
* From https://stackoverflow.com/a/66402402/3238281
*/
/**
* Mixes two hex color strings by averaging their RGB components.
*
* @param {string} color1 - The first hex color string (e.g., '#ff0000').
* @param {string} color2 - The second hex color string (e.g., '#0000ff').
* @returns {string} The resulting hex color string after mixing (e.g., '#800080').
*/
export function mixHexColors(color1: string, color2: string): string {
const RE_RGB6 = /^#[0-9a-fA-F]{6}$/
if (!RE_RGB6.test(color1) || !RE_RGB6.test(color2)) throw new Error('Invalid hex color format')
// Remove the '#' and split the hex color into RGB components
const valuesColor1 =
color1
.replace('#', '')
.match(/.{2}/g)
?.map((value) => parseInt(value, 16)) || []
const valuesColor2 =
color2
.replace('#', '')
.match(/.{2}/g)
?.map((value) => parseInt(value, 16)) || []

// Ensure both colors have valid RGB components
if (valuesColor1.length !== 3 || valuesColor2.length !== 3) {
throw new Error('Invalid hex color format')
}

// Mix the RGB components by averaging
const mixedValues = valuesColor1.map((value, index) =>
Math.round((value + valuesColor2[index]) / 2)
.toString(16)
.padStart(2, '0'),
)

return `#${mixedValues.join('')}`
}
// hexToRgb and mixHexColors functions now imported from @helpers/colors

/**
* Translate from the font name, as used in the NP Theme file,
Expand Down
59 changes: 41 additions & 18 deletions helpers/__tests__/NPThemeToCSS.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import colors from 'chalk'
import * as t from '../NPThemeToCSS'
import { hexToRgb, mixHexColors } from '../colors'
import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, Note, Paragraph } from '@mocks/index'

beforeAll(() => {
Expand Down Expand Up @@ -98,40 +99,62 @@ describe(`${FILE}`, () => {
})
})

/** mixHexColors() */
/** mixHexColors() - now in colors.js */
describe('mixHexColors()', () => {
test('should throw error on no inputs', () => {
expect(t.mixHexColors).toThrow('Invalid hex color format')
})
test('should throw error on bad inputs', () => {
test('should throw error on missing color inputs', () => {
expect(() => {
t.mixHexColors('#333', '#444')
}).toThrow('Invalid hex color format')
mixHexColors(null, null)
}).toThrow('Both colors required')
})
test('should handle 3-digit hex codes (chroma.js normalizes them)', () => {
// chroma.js handles 3-digit hex codes properly, so this should work
const res = mixHexColors('#333', '#444')
expect(res).toMatch(/^#[0-9a-f]{6}$/)
})
test('should throw error on bad inputs', () => {
test('should throw error on invalid single input', () => {
expect(() => {
t.mixHexColors('#333444')
}).toThrow('Invalid hex color format')
mixHexColors('#333444', null)
}).toThrow('Both colors required')
})
test('should return #808080', () => {
const res = t.mixHexColors('#000000', '#FFFFFF')
const res = mixHexColors('#000000', '#ffffff')
expect(res).toEqual('#808080')
})
test('should return #f8f8f8', () => {
const res = t.mixHexColors('#F0F0F0', '#FFFFFF')
const res = mixHexColors('#f0f0f0', '#ffffff')
expect(res).toEqual('#f8f8f8')
})
test('should return #f8f8f8', () => {
const res = t.mixHexColors('#FFFFFF', '#F0F0F0')
test('should return #f8f8f8 (order independent)', () => {
const res = mixHexColors('#ffffff', '#f0f0f0')
expect(res).toEqual('#f8f8f8')
})
test('should return #f7f7f7', () => {
const res = t.mixHexColors('#F0F0F0', '#FEFEFE')
const res = mixHexColors('#f0f0f0', '#fefefe')
expect(res).toEqual('#f7f7f7')
})
test('should return #f7f7f7', () => {
const res = t.mixHexColors('#F0F0F0', '#FEFEFE')
expect(res).toEqual('#f7f7f7')
})

/** hexToRgb() - now in colors.js */
describe('hexToRgb()', () => {
test('should return null for invalid input', () => {
const res = hexToRgb('invalid')
expect(res).toBeNull()
})
test('should return null for empty input', () => {
const res = hexToRgb('')
expect(res).toBeNull()
})
test('should convert 6-digit hex to RGB', () => {
const res = hexToRgb('#ff0000')
expect(res).toEqual({ r: 255, g: 0, b: 0 })
})
test('should convert 3-digit hex to RGB', () => {
const res = hexToRgb('#f00')
expect(res).toEqual({ r: 255, g: 0, b: 0 })
})
test('should handle hex without #', () => {
const res = hexToRgb('00ff00')
expect(res).toEqual({ r: 0, g: 255, b: 0 })
})
})
})
37 changes: 37 additions & 0 deletions helpers/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,40 @@ export const getAltColor = (bgColor, strength = 0.2) => {
// export const howDifferentAreTheseColors = (a: string, b: string): number => chroma.deltaE(a, b)
// NOTE: DO NOT FLOW TYPE THIS FUNCTION. IT IS IMPORTED BY JSX FILE AND FOR SOME REASON, ROLLUP CHOKES ON FLOW
export const howDifferentAreTheseColors = (a, b) => (a && b ? chroma.deltaE(a, b) : null)

/**
* Convert a hex color to RGB values using chroma.js
* @param {string} hex - Hex color string (e.g., '#ff0000')
* @returns {?{r: number, g: number, b: number}} RGB values or null if invalid
*/
// export const hexToRgb = (hex: string): ?{ r: number, g: number, b: number } => {
// NOTE: DO NOT FLOW TYPE THIS FUNCTION. IT IS IMPORTED BY JSX FILE AND FOR SOME REASON, ROLLUP CHOKES ON FLOW
export const hexToRgb = (hex) => {
try {
if (!hex) return null
const rgb = chroma(hex).rgb()
return { r: rgb[0], g: rgb[1], b: rgb[2] }
} catch (error) {
return null
}
}

/**
* Mix two hex colors by averaging them (50/50 blend) using chroma.js
* Uses RGB color space for mixing to maintain backward compatibility with simple averaging
* @param {string} color1 - The first hex color string (e.g., '#ff0000')
* @param {string} color2 - The second hex color string (e.g., '#0000ff')
* @param {number} ratio - Blend ratio from 0-1 (default 0.5 for 50/50 mix)
* @returns {string} The resulting hex color string after mixing (e.g., '#800080')
*/
// export const mixHexColors = (color1: string, color2: string, ratio: number = 0.5): string => {
// NOTE: DO NOT FLOW TYPE THIS FUNCTION. IT IS IMPORTED BY JSX FILE AND FOR SOME REASON, ROLLUP CHOKES ON FLOW
export const mixHexColors = (color1, color2, ratio = 0.5) => {
try {
if (!color1 || !color2) throw new Error('Both colors required')
// Use 'rgb' mode for simple averaging (backward compatible with original implementation)
return chroma.mix(color1, color2, ratio, 'rgb').hex()
} catch (error) {
throw new Error(`Invalid hex color format: ${error.message}`)
}
}
12 changes: 12 additions & 0 deletions helpers/react/DynamicDialog/CalendarPicker.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,25 @@

.rdp {
--rdp-cell-size: '30px';
--rdp-caption-font-size: unset !important;
--rdp-caption-label-font-size: unset !important;
box-shadow: none;
}

.rdp button {
border: none;
}

/* NP theme has border default for tables but we don't want it for buttons in the calendar picker */
.rdp-button_reset {
border: unset !important;
box-shadow: unset !important;
}

.dayPicker-container .dialogBody button {
margin-left: 0.2rem !important;
}

.calendarPickerCustom {
margin-top: 0px;
padding-top: 0px;
Expand Down
8 changes: 6 additions & 2 deletions jgclark.Dashboard/src/react/css/DashboardDialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,20 @@ dialog:modal {
/* add a clearer border to buttons */
/* border: 1px solid var(--divider-color); */
border: 1px solid rgb(from var(--fg-main-color) r g b / 0.4);
border-radius: 4px;
/* border-radius: 4px; */
padding: 1px 4px 0px 4px;
/* have margin to the right+top+bottom of buttons */
margin: 0.2rem 0.3rem 0.2rem 0; /* 3px 4px 3px 0px; */
margin: 0.2rem 0.3rem 0.2rem 0.2rem; /* 3px 4px 3px 3px; */
}
/* set FontAwesome icon colour to tint color */
.dialogBody button i {
color: var(--tint-color);
}

.dayPicker-container .dialogBody button {
margin-left: 0.2rem !important;
}

.itemActionsDialog {
max-width: 32rem;
}
Expand Down
4 changes: 4 additions & 0 deletions np.Preview/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# What's Changed in 🖥️ Previews plugin?
See [website README for more details](https://github.com/NotePlan/plugins/tree/main/np.Preview), and how to configure it.

## [0.5.0] - 2025-11-04 @dwertheimer
- improved styling of tables (border and title left justified)
- fixed HRs that did not display if they were not preceded by a blank line

## [0.4.5] - 2025-03-14
- upgraded to use Mermaid v11.x

Expand Down
4 changes: 2 additions & 2 deletions np.Preview/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"plugin.icon": "",
"plugin.author": "Jonathan Clark",
"plugin.url": "https://github.com/NotePlan/plugins/tree/main/np.Preview/",
"plugin.version": "0.4.5",
"plugin.lastUpdateInfo": "v0.4.5: updated to use Mermaid v11.\nv0.4.4: added embed images to preview, fixed some bugs",
"plugin.version": "0.5.0",
"plugin.lastUpdateInfo": "v0.5.0: improved styling of tables etc.",
"plugin.changelog": "https://github.com/NotePlan/plugins/blob/main/np.Preview/CHANGELOG.md",
"plugin.dependencies": [],
"plugin.requiredFiles": [
Expand Down