@furystack/shades-common-components 12.2.0 → 12.4.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/esm/components/form.d.ts +5 -2
  3. package/esm/components/form.d.ts.map +1 -1
  4. package/esm/components/form.js +28 -6
  5. package/esm/components/form.js.map +1 -1
  6. package/esm/components/form.spec.js +207 -0
  7. package/esm/components/form.spec.js.map +1 -1
  8. package/esm/components/index.d.ts +1 -0
  9. package/esm/components/index.d.ts.map +1 -1
  10. package/esm/components/index.js +1 -0
  11. package/esm/components/index.js.map +1 -1
  12. package/esm/components/markdown/index.d.ts +5 -0
  13. package/esm/components/markdown/index.d.ts.map +1 -0
  14. package/esm/components/markdown/index.js +5 -0
  15. package/esm/components/markdown/index.js.map +1 -0
  16. package/esm/components/markdown/markdown-display.d.ts +19 -0
  17. package/esm/components/markdown/markdown-display.d.ts.map +1 -0
  18. package/esm/components/markdown/markdown-display.js +149 -0
  19. package/esm/components/markdown/markdown-display.js.map +1 -0
  20. package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
  21. package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
  22. package/esm/components/markdown/markdown-display.spec.js +191 -0
  23. package/esm/components/markdown/markdown-display.spec.js.map +1 -0
  24. package/esm/components/markdown/markdown-editor.d.ts +25 -0
  25. package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
  26. package/esm/components/markdown/markdown-editor.js +113 -0
  27. package/esm/components/markdown/markdown-editor.js.map +1 -0
  28. package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
  29. package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
  30. package/esm/components/markdown/markdown-editor.spec.js +111 -0
  31. package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
  32. package/esm/components/markdown/markdown-input.d.ts +29 -0
  33. package/esm/components/markdown/markdown-input.d.ts.map +1 -0
  34. package/esm/components/markdown/markdown-input.js +100 -0
  35. package/esm/components/markdown/markdown-input.js.map +1 -0
  36. package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
  37. package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
  38. package/esm/components/markdown/markdown-input.spec.js +215 -0
  39. package/esm/components/markdown/markdown-input.spec.js.map +1 -0
  40. package/esm/components/markdown/markdown-parser.d.ts +82 -0
  41. package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
  42. package/esm/components/markdown/markdown-parser.js +274 -0
  43. package/esm/components/markdown/markdown-parser.js.map +1 -0
  44. package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
  45. package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
  46. package/esm/components/markdown/markdown-parser.spec.js +229 -0
  47. package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
  48. package/esm/components/styles.d.ts +1 -0
  49. package/esm/components/styles.d.ts.map +1 -1
  50. package/esm/components/styles.js.map +1 -1
  51. package/esm/components/typography.d.ts.map +1 -1
  52. package/esm/components/typography.js +26 -14
  53. package/esm/components/typography.js.map +1 -1
  54. package/esm/services/css-variable-theme.d.ts +3 -0
  55. package/esm/services/css-variable-theme.d.ts.map +1 -1
  56. package/esm/services/css-variable-theme.js +3 -0
  57. package/esm/services/css-variable-theme.js.map +1 -1
  58. package/esm/services/css-variable-theme.spec.js +3 -0
  59. package/esm/services/css-variable-theme.spec.js.map +1 -1
  60. package/esm/services/default-dark-palette.d.ts +8 -0
  61. package/esm/services/default-dark-palette.d.ts.map +1 -0
  62. package/esm/services/default-dark-palette.js +56 -0
  63. package/esm/services/default-dark-palette.js.map +1 -0
  64. package/esm/services/default-dark-theme.d.ts +3 -0
  65. package/esm/services/default-dark-theme.d.ts.map +1 -1
  66. package/esm/services/default-dark-theme.js +7 -4
  67. package/esm/services/default-dark-theme.js.map +1 -1
  68. package/esm/services/default-light-theme.d.ts +3 -0
  69. package/esm/services/default-light-theme.d.ts.map +1 -1
  70. package/esm/services/default-light-theme.js +3 -0
  71. package/esm/services/default-light-theme.js.map +1 -1
  72. package/esm/services/index.d.ts +1 -0
  73. package/esm/services/index.d.ts.map +1 -1
  74. package/esm/services/index.js +1 -0
  75. package/esm/services/index.js.map +1 -1
  76. package/esm/services/theme-provider-service.d.ts +10 -1
  77. package/esm/services/theme-provider-service.d.ts.map +1 -1
  78. package/esm/services/theme-provider-service.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/components/form.spec.tsx +309 -0
  81. package/src/components/form.tsx +31 -8
  82. package/src/components/index.ts +1 -0
  83. package/src/components/markdown/index.ts +4 -0
  84. package/src/components/markdown/markdown-display.spec.tsx +243 -0
  85. package/src/components/markdown/markdown-display.tsx +202 -0
  86. package/src/components/markdown/markdown-editor.spec.tsx +142 -0
  87. package/src/components/markdown/markdown-editor.tsx +167 -0
  88. package/src/components/markdown/markdown-input.spec.tsx +274 -0
  89. package/src/components/markdown/markdown-input.tsx +143 -0
  90. package/src/components/markdown/markdown-parser.spec.ts +258 -0
  91. package/src/components/markdown/markdown-parser.ts +333 -0
  92. package/src/components/styles.tsx +1 -0
  93. package/src/components/typography.tsx +28 -15
  94. package/src/services/css-variable-theme.spec.ts +3 -0
  95. package/src/services/css-variable-theme.ts +3 -0
  96. package/src/services/default-dark-palette.ts +57 -0
  97. package/src/services/default-dark-theme.ts +7 -4
  98. package/src/services/default-light-theme.ts +3 -0
  99. package/src/services/index.ts +1 -0
  100. package/src/services/theme-provider-service.ts +7 -1
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Zero-dependency Markdown parser that converts a Markdown string into an AST.
3
+ */
4
+
5
+ export type MarkdownNode = HeadingNode | ParagraphNode | CodeBlockNode | BlockquoteNode | ListNode | HorizontalRuleNode
6
+
7
+ export type HeadingNode = {
8
+ type: 'heading'
9
+ level: 1 | 2 | 3 | 4 | 5 | 6
10
+ children: InlineNode[]
11
+ }
12
+
13
+ export type ParagraphNode = {
14
+ type: 'paragraph'
15
+ children: InlineNode[]
16
+ }
17
+
18
+ export type CodeBlockNode = {
19
+ type: 'codeBlock'
20
+ language?: string
21
+ content: string
22
+ }
23
+
24
+ export type BlockquoteNode = {
25
+ type: 'blockquote'
26
+ children: MarkdownNode[]
27
+ }
28
+
29
+ export type ListNode = {
30
+ type: 'list'
31
+ ordered: boolean
32
+ items: ListItemNode[]
33
+ }
34
+
35
+ export type HorizontalRuleNode = {
36
+ type: 'horizontalRule'
37
+ }
38
+
39
+ export type ListItemNode = {
40
+ children: InlineNode[]
41
+ checkbox?: 'checked' | 'unchecked'
42
+ sourceLineIndex: number
43
+ }
44
+
45
+ export type InlineNode = TextNode | BoldNode | ItalicNode | InlineCodeNode | LinkNode | ImageNode
46
+
47
+ export type TextNode = { type: 'text'; content: string }
48
+ export type BoldNode = { type: 'bold'; children: InlineNode[] }
49
+ export type ItalicNode = { type: 'italic'; children: InlineNode[] }
50
+ export type InlineCodeNode = { type: 'code'; content: string }
51
+ export type LinkNode = { type: 'link'; href: string; children: InlineNode[] }
52
+ export type ImageNode = { type: 'image'; src: string; alt: string }
53
+
54
+ const HORIZONTAL_RULE_RE = /^(\*{3,}|-{3,}|_{3,})\s*$/
55
+ const HEADING_RE = /^(#{1,6})\s+(.*)/
56
+ const FENCED_CODE_OPEN_RE = /^```(\w*)\s*$/
57
+ const FENCED_CODE_CLOSE_RE = /^```\s*$/
58
+ const UNORDERED_LIST_RE = /^(\s*)[-*]\s+(.*)/
59
+ const ORDERED_LIST_RE = /^(\s*)\d+\.\s+(.*)/
60
+ const BLOCKQUOTE_RE = /^>\s?(.*)/
61
+ const CHECKBOX_UNCHECKED_RE = /^\[[ ]\]\s+(.*)/
62
+ const CHECKBOX_CHECKED_RE = /^\[[xX]\]\s+(.*)/
63
+
64
+ /**
65
+ * Parse inline Markdown formatting into an array of InlineNodes.
66
+ *
67
+ * Known limitations:
68
+ * - Backslash escapes (e.g. `\*`) are not supported; special characters are always interpreted.
69
+ * - Underscore `_` markers are not restricted to word boundaries, so identifiers like
70
+ * `some_variable_name` may produce false italic/bold matches.
71
+ */
72
+ export const parseInline = (text: string): InlineNode[] => {
73
+ const nodes: InlineNode[] = []
74
+ let pos = 0
75
+
76
+ while (pos < text.length) {
77
+ // Inline code
78
+ if (text[pos] === '`') {
79
+ const closeIdx = text.indexOf('`', pos + 1)
80
+ if (closeIdx !== -1) {
81
+ nodes.push({ type: 'code', content: text.slice(pos + 1, closeIdx) })
82
+ pos = closeIdx + 1
83
+ continue
84
+ }
85
+ }
86
+
87
+ // Image ![alt](src)
88
+ if (text[pos] === '!' && text[pos + 1] === '[') {
89
+ const altClose = text.indexOf(']', pos + 2)
90
+ if (altClose !== -1 && text[altClose + 1] === '(') {
91
+ const srcClose = text.indexOf(')', altClose + 2)
92
+ if (srcClose !== -1) {
93
+ const alt = text.slice(pos + 2, altClose)
94
+ const src = text.slice(altClose + 2, srcClose)
95
+ nodes.push({ type: 'image', src, alt })
96
+ pos = srcClose + 1
97
+ continue
98
+ }
99
+ }
100
+ }
101
+
102
+ // Link [text](href)
103
+ if (text[pos] === '[') {
104
+ const textClose = text.indexOf(']', pos + 1)
105
+ if (textClose !== -1 && text[textClose + 1] === '(') {
106
+ const hrefClose = text.indexOf(')', textClose + 2)
107
+ if (hrefClose !== -1) {
108
+ const linkText = text.slice(pos + 1, textClose)
109
+ const href = text.slice(textClose + 2, hrefClose)
110
+ nodes.push({ type: 'link', href, children: parseInline(linkText) })
111
+ pos = hrefClose + 1
112
+ continue
113
+ }
114
+ }
115
+ }
116
+
117
+ // Bold+Italic (***text***) or Bold (**text**) or Italic (*text*)
118
+ if (text[pos] === '*' || text[pos] === '_') {
119
+ const marker = text[pos]
120
+
121
+ // Count consecutive markers
122
+ let markerCount = 0
123
+ while (pos + markerCount < text.length && text[pos + markerCount] === marker) {
124
+ markerCount++
125
+ }
126
+
127
+ if (markerCount >= 3) {
128
+ const closeIdx = text.indexOf(marker.repeat(3), pos + 3)
129
+ if (closeIdx !== -1) {
130
+ const inner = text.slice(pos + 3, closeIdx)
131
+ nodes.push({ type: 'bold', children: [{ type: 'italic', children: parseInline(inner) }] })
132
+ pos = closeIdx + 3
133
+ continue
134
+ }
135
+ }
136
+
137
+ if (markerCount >= 2) {
138
+ const closeIdx = text.indexOf(marker.repeat(2), pos + 2)
139
+ if (closeIdx !== -1) {
140
+ const inner = text.slice(pos + 2, closeIdx)
141
+ nodes.push({ type: 'bold', children: parseInline(inner) })
142
+ pos = closeIdx + 2
143
+ continue
144
+ }
145
+ }
146
+
147
+ if (markerCount >= 1) {
148
+ const closeIdx = text.indexOf(marker, pos + 1)
149
+ if (closeIdx !== -1) {
150
+ const inner = text.slice(pos + 1, closeIdx)
151
+ nodes.push({ type: 'italic', children: parseInline(inner) })
152
+ pos = closeIdx + 1
153
+ continue
154
+ }
155
+ }
156
+ }
157
+
158
+ // Plain text — consume until the next special character
159
+ let end = pos + 1
160
+ while (end < text.length && !['`', '!', '[', '*', '_'].includes(text[end])) {
161
+ end++
162
+ }
163
+ const content = text.slice(pos, end)
164
+ const lastNode = nodes[nodes.length - 1]
165
+ if (lastNode?.type === 'text') {
166
+ lastNode.content += content
167
+ } else {
168
+ nodes.push({ type: 'text', content })
169
+ }
170
+ pos = end
171
+ }
172
+
173
+ return nodes
174
+ }
175
+
176
+ /**
177
+ * Parse a Markdown string into an array of block-level MarkdownNodes.
178
+ */
179
+ export const parseMarkdown = (source: string): MarkdownNode[] => {
180
+ const lines = source.split('\n')
181
+ const nodes: MarkdownNode[] = []
182
+ let i = 0
183
+
184
+ while (i < lines.length) {
185
+ const line = lines[i]
186
+
187
+ // Blank lines — skip
188
+ if (line.trim() === '') {
189
+ i++
190
+ continue
191
+ }
192
+
193
+ // Fenced code block
194
+ const codeMatch = FENCED_CODE_OPEN_RE.exec(line)
195
+ if (codeMatch) {
196
+ const language = codeMatch[1] || undefined
197
+ const codeLines: string[] = []
198
+ i++
199
+ while (i < lines.length && !FENCED_CODE_CLOSE_RE.test(lines[i])) {
200
+ codeLines.push(lines[i])
201
+ i++
202
+ }
203
+ nodes.push({ type: 'codeBlock', language, content: codeLines.join('\n') })
204
+ i++ // skip closing ```
205
+ continue
206
+ }
207
+
208
+ // Horizontal rule
209
+ if (HORIZONTAL_RULE_RE.test(line)) {
210
+ nodes.push({ type: 'horizontalRule' })
211
+ i++
212
+ continue
213
+ }
214
+
215
+ // Heading
216
+ const headingMatch = HEADING_RE.exec(line)
217
+ if (headingMatch) {
218
+ const level = headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6
219
+ nodes.push({ type: 'heading', level, children: parseInline(headingMatch[2]) })
220
+ i++
221
+ continue
222
+ }
223
+
224
+ // Blockquote
225
+ const bqMatch = BLOCKQUOTE_RE.exec(line)
226
+ if (bqMatch) {
227
+ const bqLines: string[] = []
228
+ while (i < lines.length) {
229
+ const bqLineMatch = BLOCKQUOTE_RE.exec(lines[i])
230
+ if (bqLineMatch) {
231
+ bqLines.push(bqLineMatch[1])
232
+ i++
233
+ } else {
234
+ break
235
+ }
236
+ }
237
+ nodes.push({ type: 'blockquote', children: parseMarkdown(bqLines.join('\n')) })
238
+ continue
239
+ }
240
+
241
+ // Unordered list
242
+ const ulMatch = UNORDERED_LIST_RE.exec(line)
243
+ if (ulMatch) {
244
+ const items: ListItemNode[] = []
245
+ while (i < lines.length) {
246
+ const itemMatch = UNORDERED_LIST_RE.exec(lines[i])
247
+ if (!itemMatch) break
248
+ const itemText = itemMatch[2]
249
+ const checkedMatch = CHECKBOX_CHECKED_RE.exec(itemText)
250
+ const uncheckedMatch = CHECKBOX_UNCHECKED_RE.exec(itemText)
251
+ if (checkedMatch) {
252
+ items.push({
253
+ children: parseInline(checkedMatch[1]),
254
+ checkbox: 'checked',
255
+ sourceLineIndex: i,
256
+ })
257
+ } else if (uncheckedMatch) {
258
+ items.push({
259
+ children: parseInline(uncheckedMatch[1]),
260
+ checkbox: 'unchecked',
261
+ sourceLineIndex: i,
262
+ })
263
+ } else {
264
+ items.push({
265
+ children: parseInline(itemText),
266
+ sourceLineIndex: i,
267
+ })
268
+ }
269
+ i++
270
+ }
271
+ nodes.push({ type: 'list', ordered: false, items })
272
+ continue
273
+ }
274
+
275
+ // Ordered list
276
+ const olMatch = ORDERED_LIST_RE.exec(line)
277
+ if (olMatch) {
278
+ const items: ListItemNode[] = []
279
+ while (i < lines.length) {
280
+ const itemMatch = ORDERED_LIST_RE.exec(lines[i])
281
+ if (!itemMatch) break
282
+ items.push({
283
+ children: parseInline(itemMatch[2]),
284
+ sourceLineIndex: i,
285
+ })
286
+ i++
287
+ }
288
+ nodes.push({ type: 'list', ordered: true, items })
289
+ continue
290
+ }
291
+
292
+ // Paragraph — collect consecutive non-blank, non-block-start lines
293
+ const paraLines: string[] = []
294
+ while (i < lines.length) {
295
+ const pLine = lines[i]
296
+ if (pLine.trim() === '') break
297
+ if (HEADING_RE.test(pLine)) break
298
+ if (FENCED_CODE_OPEN_RE.test(pLine)) break
299
+ if (HORIZONTAL_RULE_RE.test(pLine)) break
300
+ if (BLOCKQUOTE_RE.test(pLine)) break
301
+ if (UNORDERED_LIST_RE.test(pLine)) break
302
+ if (ORDERED_LIST_RE.test(pLine)) break
303
+ paraLines.push(pLine)
304
+ i++
305
+ }
306
+ if (paraLines.length > 0) {
307
+ nodes.push({ type: 'paragraph', children: parseInline(paraLines.join(' ')) })
308
+ }
309
+ }
310
+
311
+ return nodes
312
+ }
313
+
314
+ const TOGGLE_UNCHECKED_RE = /^(\s*[-*]\s+)\[ \]/
315
+ const TOGGLE_CHECKED_RE = /^(\s*[-*]\s+)\[[xX]\]/
316
+
317
+ /**
318
+ * Toggle a checkbox at the given source line index in the raw Markdown string.
319
+ * Only matches checkboxes in unordered list items (`- [ ]` or `* [x]`).
320
+ * Returns the updated string.
321
+ */
322
+ export const toggleCheckbox = (source: string, sourceLineIndex: number): string => {
323
+ const lines = source.split('\n')
324
+ if (sourceLineIndex < 0 || sourceLineIndex >= lines.length) return source
325
+
326
+ const line = lines[sourceLineIndex]
327
+ if (TOGGLE_UNCHECKED_RE.test(line)) {
328
+ lines[sourceLineIndex] = line.replace(TOGGLE_UNCHECKED_RE, '$1[x]')
329
+ } else if (TOGGLE_CHECKED_RE.test(line)) {
330
+ lines[sourceLineIndex] = line.replace(TOGGLE_CHECKED_RE, '$1[ ]')
331
+ }
332
+ return lines.join('\n')
333
+ }
@@ -3,6 +3,7 @@ import { cssVariableTheme } from '../services/css-variable-theme.js'
3
3
  declare global {
4
4
  interface CSSStyleDeclaration {
5
5
  backdropFilter: string
6
+ fieldSizing: string
6
7
  }
7
8
  }
8
9
 
@@ -3,8 +3,8 @@ import { Shade, createComponent } from '@furystack/shades'
3
3
  import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js'
4
4
  import { paletteMainColors } from '../services/palette-css-vars.js'
5
5
  import type { Palette } from '../services/theme-provider-service.js'
6
- import { Icon } from './icons/icon.js'
7
6
  import { clipboard } from './icons/icon-definitions.js'
7
+ import { Icon } from './icons/icon.js'
8
8
 
9
9
  /**
10
10
  * Typography variant determines semantic HTML tag and default styles.
@@ -50,74 +50,85 @@ type VariantDef = {
50
50
  lineHeight: string
51
51
  letterSpacing: string
52
52
  textTransform?: string
53
- scale?: string
53
+ marginTop?: string
54
+ marginBottom?: string
54
55
  }
55
56
 
56
57
  const variantDefs: Record<TypographyVariant, VariantDef> = {
57
58
  h1: {
58
- fontSize: cssVariableTheme.typography.fontSize.xl,
59
+ fontSize: cssVariableTheme.typography.fontSize.xxxxl,
59
60
  fontWeight: cssVariableTheme.typography.fontWeight.bold,
60
61
  lineHeight: cssVariableTheme.typography.lineHeight.tight,
61
62
  letterSpacing: cssVariableTheme.typography.letterSpacing.tight,
62
- scale: '2',
63
+ marginBottom: '0.3em',
63
64
  },
64
65
  h2: {
65
- fontSize: cssVariableTheme.typography.fontSize.xl,
66
+ fontSize: cssVariableTheme.typography.fontSize.xxxl,
66
67
  fontWeight: cssVariableTheme.typography.fontWeight.bold,
67
68
  lineHeight: cssVariableTheme.typography.lineHeight.tight,
68
69
  letterSpacing: cssVariableTheme.typography.letterSpacing.dense,
69
- scale: '1.6',
70
+ marginTop: '1.5em',
71
+ marginBottom: '0.3em',
70
72
  },
71
73
  h3: {
72
- fontSize: cssVariableTheme.typography.fontSize.xl,
74
+ fontSize: cssVariableTheme.typography.fontSize.xxl,
73
75
  fontWeight: cssVariableTheme.typography.fontWeight.semibold,
74
76
  lineHeight: cssVariableTheme.typography.lineHeight.tight,
75
77
  letterSpacing: cssVariableTheme.typography.letterSpacing.normal,
76
- scale: '1.3',
78
+ marginTop: '1.25em',
79
+ marginBottom: '0.25em',
77
80
  },
78
81
  h4: {
79
- fontSize: cssVariableTheme.typography.fontSize.lg,
82
+ fontSize: cssVariableTheme.typography.fontSize.xl,
80
83
  fontWeight: cssVariableTheme.typography.fontWeight.semibold,
81
84
  lineHeight: cssVariableTheme.typography.lineHeight.tight,
82
85
  letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
83
- scale: '1.15',
86
+ marginTop: '1em',
87
+ marginBottom: '0.25em',
84
88
  },
85
89
  h5: {
86
90
  fontSize: cssVariableTheme.typography.fontSize.lg,
87
91
  fontWeight: cssVariableTheme.typography.fontWeight.medium,
88
92
  lineHeight: cssVariableTheme.typography.lineHeight.normal,
89
93
  letterSpacing: cssVariableTheme.typography.letterSpacing.normal,
94
+ marginTop: '0.75em',
95
+ marginBottom: '0.35em',
90
96
  },
91
97
  h6: {
92
98
  fontSize: cssVariableTheme.typography.fontSize.md,
93
99
  fontWeight: cssVariableTheme.typography.fontWeight.medium,
94
100
  lineHeight: cssVariableTheme.typography.lineHeight.normal,
95
101
  letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
96
- scale: '1.1',
102
+ marginTop: '0.5em',
103
+ marginBottom: '0.2em',
97
104
  },
98
105
  subtitle1: {
99
106
  fontSize: cssVariableTheme.typography.fontSize.md,
100
107
  fontWeight: cssVariableTheme.typography.fontWeight.medium,
101
108
  lineHeight: cssVariableTheme.typography.lineHeight.normal,
102
109
  letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
110
+ marginBottom: '0.35em',
103
111
  },
104
112
  subtitle2: {
105
113
  fontSize: cssVariableTheme.typography.fontSize.sm,
106
114
  fontWeight: cssVariableTheme.typography.fontWeight.medium,
107
115
  lineHeight: cssVariableTheme.typography.lineHeight.normal,
108
116
  letterSpacing: '0.1px',
117
+ marginBottom: '0.25em',
109
118
  },
110
119
  body1: {
111
120
  fontSize: cssVariableTheme.typography.fontSize.md,
112
121
  fontWeight: cssVariableTheme.typography.fontWeight.normal,
113
122
  lineHeight: cssVariableTheme.typography.lineHeight.relaxed,
114
123
  letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
124
+ marginBottom: '0.75em',
115
125
  },
116
126
  body2: {
117
127
  fontSize: cssVariableTheme.typography.fontSize.sm,
118
128
  fontWeight: cssVariableTheme.typography.fontWeight.normal,
119
129
  lineHeight: cssVariableTheme.typography.lineHeight.relaxed,
120
130
  letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
131
+ marginBottom: '0.5em',
121
132
  },
122
133
  caption: {
123
134
  fontSize: cssVariableTheme.typography.fontSize.xs,
@@ -131,6 +142,7 @@ const variantDefs: Record<TypographyVariant, VariantDef> = {
131
142
  lineHeight: cssVariableTheme.typography.lineHeight.normal,
132
143
  letterSpacing: cssVariableTheme.typography.letterSpacing.widest,
133
144
  textTransform: 'uppercase',
145
+ marginBottom: '0.5em',
134
146
  },
135
147
  }
136
148
 
@@ -146,10 +158,11 @@ const buildVariantCssRules = (): Record<string, Record<string, string>> => {
146
158
  if (def.textTransform) {
147
159
  rule.textTransform = def.textTransform
148
160
  }
149
- if (def.scale && def.scale !== '1') {
150
- rule.transformOrigin = 'left top'
151
- rule.transform = `scale(${def.scale})`
152
- rule.marginBottom = `calc((${def.scale} - 1) * 1em)`
161
+ if (def.marginTop) {
162
+ rule.marginTop = def.marginTop
163
+ }
164
+ if (def.marginBottom) {
165
+ rule.marginBottom = def.marginBottom
153
166
  }
154
167
  rules[`&[data-variant="${variant}"]`] = rule
155
168
  }
@@ -59,6 +59,9 @@ describe('css-variable-theme', () => {
59
59
  expect(cssVariableTheme.typography.fontFamily).toBe('var(--shades-theme-typography-font-family)')
60
60
  expect(cssVariableTheme.typography.fontSize.xs).toBe('var(--shades-theme-typography-font-size-xs)')
61
61
  expect(cssVariableTheme.typography.fontSize.md).toBe('var(--shades-theme-typography-font-size-md)')
62
+ expect(cssVariableTheme.typography.fontSize.xxl).toBe('var(--shades-theme-typography-font-size-xxl)')
63
+ expect(cssVariableTheme.typography.fontSize.xxxl).toBe('var(--shades-theme-typography-font-size-xxxl)')
64
+ expect(cssVariableTheme.typography.fontSize.xxxxl).toBe('var(--shades-theme-typography-font-size-xxxxl)')
62
65
  expect(cssVariableTheme.typography.fontWeight.normal).toBe('var(--shades-theme-typography-font-weight-normal)')
63
66
  expect(cssVariableTheme.typography.fontWeight.bold).toBe('var(--shades-theme-typography-font-weight-bold)')
64
67
  expect(cssVariableTheme.typography.lineHeight.tight).toBe('var(--shades-theme-typography-line-height-tight)')
@@ -102,6 +102,9 @@ export const cssVariableTheme = {
102
102
  md: 'var(--shades-theme-typography-font-size-md)',
103
103
  lg: 'var(--shades-theme-typography-font-size-lg)',
104
104
  xl: 'var(--shades-theme-typography-font-size-xl)',
105
+ xxl: 'var(--shades-theme-typography-font-size-xxl)',
106
+ xxxl: 'var(--shades-theme-typography-font-size-xxxl)',
107
+ xxxxl: 'var(--shades-theme-typography-font-size-xxxxl)',
105
108
  },
106
109
  fontWeight: {
107
110
  normal: 'var(--shades-theme-typography-font-weight-normal)',
@@ -0,0 +1,57 @@
1
+ import type { Palette } from './theme-provider-service.js'
2
+
3
+ /**
4
+ * Color palette optimized for dark backgrounds (#121212).
5
+ * Uses lighter Material Design color variants to ensure
6
+ * WCAG AA contrast (≥4.5:1) against the dark theme background.
7
+ */
8
+ export const defaultDarkPalette: Palette = {
9
+ primary: {
10
+ light: '#9fa8da',
11
+ lightContrast: '#000000',
12
+ main: '#7986cb',
13
+ mainContrast: '#000000',
14
+ dark: '#5c6bc0',
15
+ darkContrast: '#ffffff',
16
+ },
17
+ secondary: {
18
+ light: '#4aedc4',
19
+ lightContrast: '#000000',
20
+ main: '#1de9b6',
21
+ mainContrast: '#000000',
22
+ dark: '#14a37f',
23
+ darkContrast: '#ffffff',
24
+ },
25
+ error: {
26
+ light: '#ef9a9a',
27
+ lightContrast: '#000000',
28
+ main: '#ef5350',
29
+ mainContrast: '#000000',
30
+ dark: '#e53935',
31
+ darkContrast: '#ffffff',
32
+ },
33
+ warning: {
34
+ light: '#ffb74d',
35
+ lightContrast: '#000000',
36
+ main: '#ff9800',
37
+ mainContrast: '#000000',
38
+ dark: '#f57c00',
39
+ darkContrast: '#000000',
40
+ },
41
+ info: {
42
+ light: '#90caf9',
43
+ lightContrast: '#000000',
44
+ main: '#42a5f5',
45
+ mainContrast: '#000000',
46
+ dark: '#1e88e5',
47
+ darkContrast: '#ffffff',
48
+ },
49
+ success: {
50
+ light: '#81c784',
51
+ lightContrast: '#000000',
52
+ main: '#4caf50',
53
+ mainContrast: '#000000',
54
+ dark: '#388e3c',
55
+ darkContrast: '#ffffff',
56
+ },
57
+ }
@@ -1,4 +1,4 @@
1
- import { defaultPalette } from './default-palette.js'
1
+ import { defaultDarkPalette } from './default-dark-palette.js'
2
2
  import type { Theme } from './theme-provider-service.js'
3
3
 
4
4
  export const defaultDarkTheme = {
@@ -16,10 +16,10 @@ export const defaultDarkTheme = {
16
16
  disabledBackground: 'rgba(255, 255, 255, 0.12)',
17
17
  },
18
18
  background: {
19
- default: '#303030',
20
- paper: '#424242',
19
+ default: '#121212',
20
+ paper: '#1e1e1e',
21
21
  },
22
- palette: defaultPalette,
22
+ palette: defaultDarkPalette,
23
23
  divider: 'rgba(255, 255, 255, 0.12)',
24
24
  action: {
25
25
  hoverBackground: 'rgba(255, 255, 255, 0.08)',
@@ -54,6 +54,9 @@ export const defaultDarkTheme = {
54
54
  md: '14px',
55
55
  lg: '16px',
56
56
  xl: '24px',
57
+ xxl: '30px',
58
+ xxxl: '36px',
59
+ xxxxl: '48px',
57
60
  },
58
61
  fontWeight: {
59
62
  normal: '400',
@@ -54,6 +54,9 @@ export const defaultLightTheme = {
54
54
  md: '14px',
55
55
  lg: '16px',
56
56
  xl: '24px',
57
+ xxl: '30px',
58
+ xxxl: '36px',
59
+ xxxxl: '48px',
57
60
  },
58
61
  fontWeight: {
59
62
  normal: '400',
@@ -1,6 +1,7 @@
1
1
  export * from './click-away-service.js'
2
2
  export * from './collection-service.js'
3
3
  export * from './css-variable-theme.js'
4
+ export * from './default-dark-palette.js'
4
5
  export * from './default-dark-theme.js'
5
6
  export * from './default-light-theme.js'
6
7
  export * from './default-palette.js'
@@ -158,8 +158,14 @@ export type FontSizeScale = {
158
158
  md: string
159
159
  /** 16px - large body, icons */
160
160
  lg: string
161
- /** 24px - page headings */
161
+ /** 24px - subheadings, large UI elements */
162
162
  xl: string
163
+ /** 30px - small headings (h3) */
164
+ xxl: string
165
+ /** 36px - medium headings (h2) */
166
+ xxxl: string
167
+ /** 48px - large headings (h1) */
168
+ xxxxl: string
163
169
  }
164
170
 
165
171
  /**