@nons-dev/uikit 0.1.7 → 0.1.8
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.
- package/core/config/index.ts +76 -0
- package/core/config/parser.ts +112 -0
- package/core/config/types.ts +41 -0
- package/core/config/validator.ts +68 -0
- package/dist/index.js +1214 -1155
- package/dist/src/index.d.ts +2 -2
- package/package.json +8 -1
- package/readme.md +67 -55
- package/scripts/vite-plugin-nons-config.ts +76 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { NonsConfig, RawConfig } from './types'
|
|
2
|
+
import { validateConfig } from './validator'
|
|
3
|
+
import type { ValidationResult } from './validator'
|
|
4
|
+
|
|
5
|
+
export type { NonsConfig, RawConfig, ValidationResult }
|
|
6
|
+
export { generateFullCSS } from './parser'
|
|
7
|
+
|
|
8
|
+
let _config: NonsConfig | null = null
|
|
9
|
+
let _rawConfig: RawConfig | null = null
|
|
10
|
+
|
|
11
|
+
export function createConfig(overrides?: Partial<NonsConfig>): NonsConfig {
|
|
12
|
+
return {
|
|
13
|
+
theme: { preset: 'nons' },
|
|
14
|
+
brand: { name: 'Nons' },
|
|
15
|
+
direction: { default: 'rtl' },
|
|
16
|
+
locale: { default: 'fa', supported: ['fa', 'en'] },
|
|
17
|
+
font: { fa: { family: 'IRANSansX' }, en: { family: 'Satoshi' } },
|
|
18
|
+
...overrides,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadConfig(config: NonsConfig) {
|
|
23
|
+
_config = config
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadRawConfig(config: RawConfig) {
|
|
27
|
+
_rawConfig = config
|
|
28
|
+
_config = {
|
|
29
|
+
theme: config.theme,
|
|
30
|
+
brand: config.brand,
|
|
31
|
+
direction: config.direction,
|
|
32
|
+
locale: config.locale,
|
|
33
|
+
font: config.font,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getConfig(): NonsConfig {
|
|
38
|
+
if (!_config) {
|
|
39
|
+
throw new Error('Config not loaded. Call loadConfig() first.')
|
|
40
|
+
}
|
|
41
|
+
return _config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getRawConfig(): RawConfig {
|
|
45
|
+
if (!_rawConfig) {
|
|
46
|
+
throw new Error('Raw config not loaded. Call loadRawConfig() first.')
|
|
47
|
+
}
|
|
48
|
+
return _rawConfig
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateNonsConfig(config: unknown): ValidationResult {
|
|
52
|
+
return validateConfig(config)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getThemePreset(): string {
|
|
56
|
+
return getConfig().theme.preset
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getDirection(): 'rtl' | 'ltr' {
|
|
60
|
+
return getConfig().direction.default as 'rtl' | 'ltr'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getDefaultLocale(): string {
|
|
64
|
+
return getConfig().locale.default
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getSupportedLocales(): string[] {
|
|
68
|
+
return getConfig().locale.supported
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getFontFamily(locale: string): string {
|
|
72
|
+
const fonts = getConfig().font
|
|
73
|
+
if (locale === 'fa' && fonts.fa?.family) return fonts.fa.family
|
|
74
|
+
if (fonts.en?.family) return fonts.en.family
|
|
75
|
+
return 'IRANSansX'
|
|
76
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { RawConfig } from './types'
|
|
2
|
+
|
|
3
|
+
function writeTokenBlock(tokens: Record<string, string>, indent: string): string {
|
|
4
|
+
return Object.entries(tokens || {})
|
|
5
|
+
.map(([k, v]) => `${indent}${k}: ${v};`)
|
|
6
|
+
.join('\n')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function generateFontCSS(config: RawConfig): string {
|
|
10
|
+
const faces = config.font?.faces
|
|
11
|
+
if (!faces || !faces.length) return ''
|
|
12
|
+
return faces.map(face =>
|
|
13
|
+
`@font-face {
|
|
14
|
+
font-family: '${face.family}';
|
|
15
|
+
src: url('./font/${face.src.split('/').pop()}') format('${face.format}');
|
|
16
|
+
font-weight: ${face.weight};
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}`
|
|
20
|
+
).join('\n\n') + '\n\n'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateTokensCSS(config: RawConfig): string {
|
|
24
|
+
const lines: string[] = []
|
|
25
|
+
const fonts = generateFontCSS(config)
|
|
26
|
+
if (fonts) lines.push(fonts)
|
|
27
|
+
|
|
28
|
+
const t = config.tokens
|
|
29
|
+
const hasLight = t.colors?.light && Object.keys(t.colors.light).length
|
|
30
|
+
const hasDark = t.colors?.dark && Object.keys(t.colors.dark).length
|
|
31
|
+
|
|
32
|
+
if (hasLight) {
|
|
33
|
+
lines.push(':root {')
|
|
34
|
+
lines.push(' color-scheme: light;')
|
|
35
|
+
lines.push(writeTokenBlock(t.colors.light, ' '))
|
|
36
|
+
lines.push('}')
|
|
37
|
+
lines.push('')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hasDark) {
|
|
41
|
+
lines.push('[data-theme="dark"] {')
|
|
42
|
+
lines.push(' color-scheme: dark;')
|
|
43
|
+
lines.push(writeTokenBlock(t.colors.dark, ' '))
|
|
44
|
+
lines.push('}')
|
|
45
|
+
lines.push('')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const flatSections: [string, string][] = [
|
|
49
|
+
['spacing', 'Spacing'],
|
|
50
|
+
['typography', 'Typography'],
|
|
51
|
+
['radius', 'Border Radius'],
|
|
52
|
+
['z-index', 'Z-Index'],
|
|
53
|
+
['opacity', 'Opacity'],
|
|
54
|
+
['button', 'Button'],
|
|
55
|
+
['alert', 'Alert'],
|
|
56
|
+
]
|
|
57
|
+
for (const [section, label] of flatSections) {
|
|
58
|
+
const tokens = (t as any)[section] as Record<string, string> | undefined
|
|
59
|
+
if (!tokens || !Object.keys(tokens).length) continue
|
|
60
|
+
lines.push(`/* ${label} */`)
|
|
61
|
+
lines.push(':root {')
|
|
62
|
+
lines.push(writeTokenBlock(tokens, ' '))
|
|
63
|
+
lines.push('}')
|
|
64
|
+
lines.push('')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasShadowLight = t.shadows?.light && Object.keys(t.shadows.light).length
|
|
68
|
+
const hasShadowDark = t.shadows?.dark && Object.keys(t.shadows.dark).length
|
|
69
|
+
if (hasShadowLight) {
|
|
70
|
+
lines.push(':root {')
|
|
71
|
+
lines.push(' color-scheme: light;')
|
|
72
|
+
lines.push(writeTokenBlock(t.shadows.light, ' '))
|
|
73
|
+
lines.push('}')
|
|
74
|
+
lines.push('')
|
|
75
|
+
}
|
|
76
|
+
if (hasShadowDark) {
|
|
77
|
+
lines.push('[data-theme="dark"] {')
|
|
78
|
+
lines.push(' color-scheme: dark;')
|
|
79
|
+
lines.push(writeTokenBlock(t.shadows.dark, ' '))
|
|
80
|
+
lines.push('}')
|
|
81
|
+
lines.push('')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function generateThemeCSS(config: RawConfig): string {
|
|
88
|
+
const m = config['theme-mappings']
|
|
89
|
+
if (!m) return ''
|
|
90
|
+
|
|
91
|
+
const lines: string[] = []
|
|
92
|
+
lines.push(':root {')
|
|
93
|
+
lines.push(' color-scheme: light;')
|
|
94
|
+
if (m.light) lines.push(writeTokenBlock(m.light, ' '))
|
|
95
|
+
lines.push('}')
|
|
96
|
+
lines.push('')
|
|
97
|
+
lines.push('[data-theme="dark"] {')
|
|
98
|
+
lines.push(' color-scheme: dark;')
|
|
99
|
+
if (m.dark) lines.push(writeTokenBlock(m.dark, ' '))
|
|
100
|
+
lines.push('}')
|
|
101
|
+
|
|
102
|
+
return lines.join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function generateFullCSS(config: RawConfig): string {
|
|
106
|
+
const parts: string[] = []
|
|
107
|
+
const tokens = generateTokensCSS(config)
|
|
108
|
+
if (tokens) parts.push(tokens)
|
|
109
|
+
const theme = generateThemeCSS(config)
|
|
110
|
+
if (theme) parts.push(theme)
|
|
111
|
+
return parts.join('\n')
|
|
112
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface FontFace {
|
|
2
|
+
family: string
|
|
3
|
+
weight: number
|
|
4
|
+
src: string
|
|
5
|
+
format: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NonsConfig {
|
|
9
|
+
theme: { preset: string }
|
|
10
|
+
brand: { name: string }
|
|
11
|
+
direction: { default: string }
|
|
12
|
+
locale: { default: string; supported: string[] }
|
|
13
|
+
font: {
|
|
14
|
+
fa?: { family: string }
|
|
15
|
+
en?: { family: string }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RawConfig {
|
|
20
|
+
theme: { preset: string }
|
|
21
|
+
brand: { name: string }
|
|
22
|
+
direction: { default: string }
|
|
23
|
+
locale: { default: string; supported: string[] }
|
|
24
|
+
font: {
|
|
25
|
+
fa?: { family: string }
|
|
26
|
+
en?: { family: string }
|
|
27
|
+
faces?: FontFace[]
|
|
28
|
+
}
|
|
29
|
+
tokens: {
|
|
30
|
+
colors: { light: Record<string, string>; dark: Record<string, string> }
|
|
31
|
+
spacing: Record<string, string>
|
|
32
|
+
typography: Record<string, string>
|
|
33
|
+
radius: Record<string, string>
|
|
34
|
+
shadows: { light: Record<string, string>; dark: Record<string, string> }
|
|
35
|
+
'z-index': Record<string, string>
|
|
36
|
+
opacity: Record<string, string>
|
|
37
|
+
button: Record<string, string>
|
|
38
|
+
alert: Record<string, string>
|
|
39
|
+
}
|
|
40
|
+
'theme-mappings': { light: Record<string, string>; dark: Record<string, string> }
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean
|
|
3
|
+
errors: string[]
|
|
4
|
+
warnings: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function validateConfig(config: unknown): ValidationResult {
|
|
8
|
+
const errors: string[] = []
|
|
9
|
+
const warnings: string[] = []
|
|
10
|
+
|
|
11
|
+
if (!config || typeof config !== 'object') {
|
|
12
|
+
errors.push('Config must be a non-null object')
|
|
13
|
+
return { valid: false, errors, warnings }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const c = config as Record<string, unknown>
|
|
17
|
+
|
|
18
|
+
if (!c.theme || typeof c.theme !== 'object') {
|
|
19
|
+
errors.push('Missing or invalid "theme" section')
|
|
20
|
+
} else {
|
|
21
|
+
const theme = c.theme as Record<string, unknown>
|
|
22
|
+
if (!theme.preset || typeof theme.preset !== 'string') {
|
|
23
|
+
errors.push('theme.preset must be a non-empty string')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!c.brand || typeof c.brand !== 'object') {
|
|
28
|
+
errors.push('Missing or invalid "brand" section')
|
|
29
|
+
} else {
|
|
30
|
+
const brand = c.brand as Record<string, unknown>
|
|
31
|
+
if (!brand.name || typeof brand.name !== 'string') {
|
|
32
|
+
errors.push('brand.name must be a non-empty string')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!c.direction || typeof c.direction !== 'object') {
|
|
37
|
+
errors.push('Missing or invalid "direction" section')
|
|
38
|
+
} else {
|
|
39
|
+
const dir = c.direction as Record<string, unknown>
|
|
40
|
+
if (dir.default !== 'rtl' && dir.default !== 'ltr') {
|
|
41
|
+
errors.push('direction.default must be "rtl" or "ltr"')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!c.locale || typeof c.locale !== 'object') {
|
|
46
|
+
errors.push('Missing or invalid "locale" section')
|
|
47
|
+
} else {
|
|
48
|
+
const loc = c.locale as Record<string, unknown>
|
|
49
|
+
if (!loc.default || typeof loc.default !== 'string') {
|
|
50
|
+
errors.push('locale.default must be a non-empty string')
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(loc.supported) || (loc.supported as unknown[]).length === 0) {
|
|
53
|
+
errors.push('locale.supported must be a non-empty array')
|
|
54
|
+
} else if (typeof loc.default === 'string' && !(loc.supported as string[]).includes(loc.default)) {
|
|
55
|
+
warnings.push('locale.default is not in locale.supported')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!c.tokens || typeof c.tokens !== 'object') {
|
|
60
|
+
errors.push('Missing or invalid "tokens" section')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
valid: errors.length === 0,
|
|
65
|
+
errors,
|
|
66
|
+
warnings,
|
|
67
|
+
}
|
|
68
|
+
}
|