@charcoal-ui/styled 1.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +17 -0
- package/package.json +54 -0
- package/src/index.story.tsx +318 -0
- package/src/index.ts +720 -0
- package/src/lib.ts +178 -0
- package/src/theme.ts +107 -0
- package/src/types.ts +43 -0
- package/src/util.ts +78 -0
- package/theme.json +676 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { CSSObject, ThemedStyledInterface } from 'styled-components'
|
|
2
|
+
import warning from 'warning'
|
|
3
|
+
import {
|
|
4
|
+
factory,
|
|
5
|
+
modifiedFactory,
|
|
6
|
+
constFactory,
|
|
7
|
+
modifiedArgumentedFactory,
|
|
8
|
+
} from './lib'
|
|
9
|
+
import { EffectType, StyledTheme as Theme, Key } from './types'
|
|
10
|
+
import {
|
|
11
|
+
objectAssign,
|
|
12
|
+
unreachable,
|
|
13
|
+
ReadonlyArrayConstructor,
|
|
14
|
+
objectKeys,
|
|
15
|
+
isPresent,
|
|
16
|
+
} from './util'
|
|
17
|
+
import { columnPx, halfLeading } from '@charcoal-ui/foundation'
|
|
18
|
+
import {
|
|
19
|
+
applyEffect,
|
|
20
|
+
applyEffectToGradient,
|
|
21
|
+
dur,
|
|
22
|
+
gradient,
|
|
23
|
+
GradientDirection,
|
|
24
|
+
notDisabledSelector,
|
|
25
|
+
disabledSelector,
|
|
26
|
+
px,
|
|
27
|
+
} from '@charcoal-ui/utils'
|
|
28
|
+
export { type Modified, type ModifiedArgumented } from './lib'
|
|
29
|
+
|
|
30
|
+
import { light, dark } from './theme'
|
|
31
|
+
export { type ElementsTheme } from './theme'
|
|
32
|
+
|
|
33
|
+
const colorProperties = ['bg', 'font'] as const
|
|
34
|
+
type ColorProperty = typeof colorProperties[number]
|
|
35
|
+
|
|
36
|
+
const spacingProperties = ['margin', 'padding'] as const
|
|
37
|
+
const spacingDirections = [
|
|
38
|
+
'top',
|
|
39
|
+
'right',
|
|
40
|
+
'bottom',
|
|
41
|
+
'left',
|
|
42
|
+
'vertical',
|
|
43
|
+
'horizontal',
|
|
44
|
+
'all',
|
|
45
|
+
] as const
|
|
46
|
+
type SpacingProperty = typeof spacingProperties[number]
|
|
47
|
+
type SpacingDirection = typeof spacingDirections[number]
|
|
48
|
+
|
|
49
|
+
const fixedProperties = ['width', 'height'] as const
|
|
50
|
+
type FixedProperty = typeof fixedProperties[number]
|
|
51
|
+
|
|
52
|
+
const borderDirections = ['top', 'right', 'bottom', 'left'] as const
|
|
53
|
+
type BorderDirection = typeof borderDirections[number]
|
|
54
|
+
|
|
55
|
+
const outlineType = ['focus'] as const
|
|
56
|
+
type OutlineType = typeof outlineType[number]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `theme(o => [...])` の `o` の部分を構築する
|
|
60
|
+
*
|
|
61
|
+
* @param theme テーマオブジェクト
|
|
62
|
+
* @param isPhantom 型推論のためだけに使う場合にランタイムコストをゼロにするフラグ
|
|
63
|
+
*/
|
|
64
|
+
function builder<T extends Theme>(
|
|
65
|
+
theme: {
|
|
66
|
+
// factoryの第二引数に入れ込むものだけ明示的に型変数を展開しておくことで型の具象化を遅延する
|
|
67
|
+
color: T['color']
|
|
68
|
+
gradientColor: T['gradientColor']
|
|
69
|
+
border: T['border']
|
|
70
|
+
outline: T['outline']
|
|
71
|
+
} & Omit<T, 'color' | 'gradientColor' | 'border' | 'outline'>,
|
|
72
|
+
isPhantom = false
|
|
73
|
+
) {
|
|
74
|
+
if (isPhantom) {
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
76
|
+
return {} as never
|
|
77
|
+
}
|
|
78
|
+
const colors = objectKeys(theme.color)
|
|
79
|
+
const effects = objectKeys(theme.effect)
|
|
80
|
+
|
|
81
|
+
// 色
|
|
82
|
+
const gradientColors = objectKeys(theme.gradientColor)
|
|
83
|
+
const colorCss = createColorCss(theme)
|
|
84
|
+
const gradientColorCss = createGradientColorCss(theme)
|
|
85
|
+
const colorObject = constFactory(
|
|
86
|
+
{},
|
|
87
|
+
{
|
|
88
|
+
bg: objectAssign(
|
|
89
|
+
factory({}, colors, (color) =>
|
|
90
|
+
modifiedFactory(effects, (modifiers) =>
|
|
91
|
+
colorCss('bg', color, modifiers)
|
|
92
|
+
)
|
|
93
|
+
),
|
|
94
|
+
factory(
|
|
95
|
+
{},
|
|
96
|
+
gradientColors,
|
|
97
|
+
(color) => (direction: GradientDirection) =>
|
|
98
|
+
modifiedFactory(effects, (modifiers) =>
|
|
99
|
+
gradientColorCss(color, modifiers, direction)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
font: factory({}, colors, (color) =>
|
|
104
|
+
modifiedFactory(effects, (modifiers) =>
|
|
105
|
+
colorCss('font', color, modifiers)
|
|
106
|
+
)
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// タイポグラフィ
|
|
112
|
+
const typographyModifiers = [
|
|
113
|
+
// TODO
|
|
114
|
+
'monospace',
|
|
115
|
+
'bold',
|
|
116
|
+
'preserveHalfLeading',
|
|
117
|
+
] as const
|
|
118
|
+
const typographyCss = createTypographyCss(theme)
|
|
119
|
+
const typographyObject = factory(
|
|
120
|
+
{},
|
|
121
|
+
['typography'] as const,
|
|
122
|
+
(_) => (size: keyof T['typography']['size']) =>
|
|
123
|
+
modifiedFactory(typographyModifiers, (modifiers) =>
|
|
124
|
+
typographyCss(size, {
|
|
125
|
+
preserveHalfLeading: modifiers.includes('preserveHalfLeading'),
|
|
126
|
+
monospace: modifiers.includes('monospace'),
|
|
127
|
+
bold: modifiers.includes('bold'),
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// スペーシング
|
|
133
|
+
const spacingCss = createSpacingCss(theme)
|
|
134
|
+
const spacingObject = factory({}, spacingProperties, (spacingProperty) =>
|
|
135
|
+
modifiedArgumentedFactory(
|
|
136
|
+
spacingDirections,
|
|
137
|
+
(modifiers) => spacingCss(spacingProperty, modifiers),
|
|
138
|
+
{} as keyof T['spacing'] | 'auto' // 推論のためのメタタイプ
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// 大きさ
|
|
143
|
+
const fixedPxCss = createFixedPxCss(theme)
|
|
144
|
+
const fixedColumnCss = createFixedColumnCss(theme)
|
|
145
|
+
const fixedRelativeCss = createFixedRelativeCss(theme)
|
|
146
|
+
const fixedObject = factory({}, fixedProperties, (property) =>
|
|
147
|
+
constFactory(
|
|
148
|
+
{},
|
|
149
|
+
{
|
|
150
|
+
px: (size: keyof T['spacing'] | 'auto') => fixedPxCss(property, size),
|
|
151
|
+
column: (span: number) => fixedColumnCss(property, span),
|
|
152
|
+
auto: fixedRelativeCss(property, 'auto'),
|
|
153
|
+
full: fixedRelativeCss(property, '100%'),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// 要素へのエフェクト (etc: 透過)
|
|
159
|
+
const elementEffectCss = createElementEffectCss(theme)
|
|
160
|
+
const elementEffectObject = modifiedFactory(
|
|
161
|
+
objectKeys(theme.elementEffect),
|
|
162
|
+
(modifiers) => elementEffectCss(modifiers)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// ボーダー
|
|
166
|
+
const borderCss = createBorderCss(theme)
|
|
167
|
+
const borderObject = constFactory(
|
|
168
|
+
{},
|
|
169
|
+
{
|
|
170
|
+
border: factory({}, objectKeys(theme.border), (variant) =>
|
|
171
|
+
modifiedFactory(borderDirections, (modifiers) =>
|
|
172
|
+
borderCss(variant, modifiers)
|
|
173
|
+
)
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// 角丸
|
|
179
|
+
const borderRadiusCss = createBorderRadiusCss(theme)
|
|
180
|
+
const borderRadiusObject = constFactory(
|
|
181
|
+
{},
|
|
182
|
+
{
|
|
183
|
+
borderRadius: (radius: keyof T['borderRadius']) =>
|
|
184
|
+
borderRadiusCss(radius),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// アウトライン
|
|
189
|
+
const outlineCss = createOutlineColorCss(theme)
|
|
190
|
+
const outlineObject = constFactory(
|
|
191
|
+
{},
|
|
192
|
+
{
|
|
193
|
+
outline: factory({}, objectKeys(theme.outline), (variant) =>
|
|
194
|
+
modifiedFactory(outlineType, (modifiers) =>
|
|
195
|
+
outlineCss(variant, modifiers)
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return objectAssign(
|
|
202
|
+
colorObject,
|
|
203
|
+
typographyObject,
|
|
204
|
+
spacingObject,
|
|
205
|
+
fixedObject,
|
|
206
|
+
elementEffectObject,
|
|
207
|
+
borderObject,
|
|
208
|
+
borderRadiusObject,
|
|
209
|
+
outlineObject
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function targetProperty(target: ColorProperty) {
|
|
214
|
+
return target === 'bg' ? 'background-color' : 'color'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isSupportedEffect(effect: Key): effect is EffectType {
|
|
218
|
+
return ['hover', 'press', 'disabled'].includes(effect as string)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function onEffectPseudo(effect: EffectType, css: CSSObject) {
|
|
222
|
+
return effect === 'hover'
|
|
223
|
+
? { '&:hover': { [notDisabledSelector]: css } }
|
|
224
|
+
: effect === 'press'
|
|
225
|
+
? { '&:active': { [notDisabledSelector]: css } }
|
|
226
|
+
: // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
227
|
+
effect === 'disabled'
|
|
228
|
+
? { [disabledSelector]: css }
|
|
229
|
+
: unreachable(effect)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const createColorCss =
|
|
233
|
+
<T extends Theme>(theme: T) =>
|
|
234
|
+
(
|
|
235
|
+
target: ColorProperty,
|
|
236
|
+
color: keyof T['color'],
|
|
237
|
+
effects: readonly (keyof T['effect'])[] = []
|
|
238
|
+
): Internal =>
|
|
239
|
+
internal(
|
|
240
|
+
() => ({
|
|
241
|
+
[targetProperty(target)]: theme.color[color],
|
|
242
|
+
...effects.filter(isSupportedEffect).reduce<CSSObject>(
|
|
243
|
+
(acc, effect) => ({
|
|
244
|
+
...acc,
|
|
245
|
+
...onEffectPseudo(effect, {
|
|
246
|
+
[targetProperty(target)]: applyEffect(
|
|
247
|
+
theme.color[color],
|
|
248
|
+
theme.effect[effect] ?? []
|
|
249
|
+
),
|
|
250
|
+
}),
|
|
251
|
+
}),
|
|
252
|
+
{}
|
|
253
|
+
),
|
|
254
|
+
}),
|
|
255
|
+
effects.length > 0
|
|
256
|
+
? target === 'font'
|
|
257
|
+
? {
|
|
258
|
+
colorTransition: true,
|
|
259
|
+
}
|
|
260
|
+
: {
|
|
261
|
+
backgroundColorTransition: true,
|
|
262
|
+
}
|
|
263
|
+
: {}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
// TODO: deprecate
|
|
267
|
+
const TRANSITION_DURATION = 0.2
|
|
268
|
+
|
|
269
|
+
const createGradientColorCss =
|
|
270
|
+
<T extends Theme>(theme: T) =>
|
|
271
|
+
(
|
|
272
|
+
color: keyof T['gradientColor'],
|
|
273
|
+
effects: readonly (keyof T['effect'])[] = [],
|
|
274
|
+
direction: GradientDirection
|
|
275
|
+
): Internal => {
|
|
276
|
+
const toLinearGradient = gradient(direction)
|
|
277
|
+
return internal((context) => {
|
|
278
|
+
const optimized = !useHalfLeadingCanceller(context)
|
|
279
|
+
const duration = dur(TRANSITION_DURATION)
|
|
280
|
+
if (optimized && effects.length > 0) {
|
|
281
|
+
return {
|
|
282
|
+
position: 'relative',
|
|
283
|
+
zIndex: 0,
|
|
284
|
+
overflow: 'hidden',
|
|
285
|
+
...effects.filter(isSupportedEffect).reduce<CSSObject>(
|
|
286
|
+
(acc, effect) => ({
|
|
287
|
+
...acc,
|
|
288
|
+
'&::before': {
|
|
289
|
+
zIndex: -1,
|
|
290
|
+
...overlayElement,
|
|
291
|
+
transition: `${duration} background-color`,
|
|
292
|
+
},
|
|
293
|
+
'&::after': {
|
|
294
|
+
zIndex: -2,
|
|
295
|
+
...overlayElement,
|
|
296
|
+
...toLinearGradient(theme.gradientColor[color]),
|
|
297
|
+
},
|
|
298
|
+
...onEffectPseudo(effect, {
|
|
299
|
+
'&::before': {
|
|
300
|
+
backgroundColor: applyEffect(
|
|
301
|
+
null,
|
|
302
|
+
theme.effect[effect] ?? []
|
|
303
|
+
),
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
}),
|
|
307
|
+
{}
|
|
308
|
+
),
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
warning(
|
|
312
|
+
effects.length === 0,
|
|
313
|
+
// eslint-disable-next-line max-len
|
|
314
|
+
`'Transition' will not be applied. You can get around this by specifying 'preserveHalfLeading' or both 'padding' and 'typograpy'.`
|
|
315
|
+
)
|
|
316
|
+
return {
|
|
317
|
+
...toLinearGradient(theme.gradientColor[color]),
|
|
318
|
+
...effects.filter(isSupportedEffect).reduce<CSSObject>(
|
|
319
|
+
(acc, effect) => ({
|
|
320
|
+
...acc,
|
|
321
|
+
...onEffectPseudo(effect, {
|
|
322
|
+
...toLinearGradient(
|
|
323
|
+
applyEffectToGradient(theme.effect[effect] ?? [])(
|
|
324
|
+
theme.gradientColor[color]
|
|
325
|
+
)
|
|
326
|
+
),
|
|
327
|
+
}),
|
|
328
|
+
}),
|
|
329
|
+
{}
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @see https://developer.mozilla.org/ja/docs/Web/CSS/:focus-visible#selectively_showing_the_focus_indicator
|
|
338
|
+
*/
|
|
339
|
+
const onFocus = (css: CSSObject) => ({
|
|
340
|
+
[notDisabledSelector]: {
|
|
341
|
+
'&:focus, &:active': {
|
|
342
|
+
outline: 'none',
|
|
343
|
+
...css,
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
'&:focus:not(:focus-visible), &:active:not(:focus-visible)': {
|
|
347
|
+
outline: 'none',
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
'&:focus-visible': {
|
|
351
|
+
outline: 'none',
|
|
352
|
+
...css,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const outlineCss = (weight: number, color: string) => ({
|
|
358
|
+
boxShadow: `0 0 0 ${px(weight)} ${color}`,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const createOutlineColorCss =
|
|
362
|
+
<T extends Theme>(theme: T) =>
|
|
363
|
+
(
|
|
364
|
+
variant: keyof T['outline'],
|
|
365
|
+
modifiers: readonly OutlineType[]
|
|
366
|
+
): Internal => {
|
|
367
|
+
const weight = theme.outline[variant].weight
|
|
368
|
+
const color = theme.outline[variant].color
|
|
369
|
+
return internal(
|
|
370
|
+
() =>
|
|
371
|
+
modifiers.includes('focus')
|
|
372
|
+
? onFocus(outlineCss(weight, color))
|
|
373
|
+
: { '&&': { [notDisabledSelector]: outlineCss(weight, color) } },
|
|
374
|
+
{
|
|
375
|
+
boxShadowTransition: true,
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const overlayElement: CSSObject = {
|
|
381
|
+
content: "''",
|
|
382
|
+
display: 'block',
|
|
383
|
+
position: 'absolute',
|
|
384
|
+
width: '100%',
|
|
385
|
+
height: '100%',
|
|
386
|
+
top: 0,
|
|
387
|
+
left: 0,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// half-leadingをキャンセルするとき && 垂直方向のpaddingが無い時
|
|
391
|
+
// -> before/afterを入れる
|
|
392
|
+
const useHalfLeadingCanceller = ({
|
|
393
|
+
cancelHalfLeadingPx,
|
|
394
|
+
hasVerticalPadding = false,
|
|
395
|
+
}: Context) => cancelHalfLeadingPx !== undefined && !hasVerticalPadding
|
|
396
|
+
|
|
397
|
+
const createTypographyCss =
|
|
398
|
+
<T extends Theme>(theme: T) =>
|
|
399
|
+
(
|
|
400
|
+
size: keyof T['typography']['size'],
|
|
401
|
+
options: {
|
|
402
|
+
preserveHalfLeading?: boolean
|
|
403
|
+
monospace?: boolean
|
|
404
|
+
bold?: boolean
|
|
405
|
+
} = {}
|
|
406
|
+
): Internal => {
|
|
407
|
+
const {
|
|
408
|
+
preserveHalfLeading = false,
|
|
409
|
+
monospace = false,
|
|
410
|
+
bold = false,
|
|
411
|
+
} = options
|
|
412
|
+
const descriptor = theme.typography.size[size]
|
|
413
|
+
const margin = -halfLeading(descriptor)
|
|
414
|
+
|
|
415
|
+
return internal(
|
|
416
|
+
(context) => ({
|
|
417
|
+
fontSize: px(descriptor.fontSize),
|
|
418
|
+
lineHeight: px(descriptor.lineHeight),
|
|
419
|
+
...(monospace && {
|
|
420
|
+
fontFamily: 'monospace',
|
|
421
|
+
}),
|
|
422
|
+
...(bold && {
|
|
423
|
+
fontWeight: 'bold',
|
|
424
|
+
}),
|
|
425
|
+
...(useHalfLeadingCanceller(context) && {
|
|
426
|
+
// prevent margin collapsing
|
|
427
|
+
display: 'flow-root',
|
|
428
|
+
// cancel half-leading with negative margin
|
|
429
|
+
'&::before': {
|
|
430
|
+
...leadingCancel,
|
|
431
|
+
marginTop: px(margin),
|
|
432
|
+
},
|
|
433
|
+
'&::after': {
|
|
434
|
+
...leadingCancel,
|
|
435
|
+
marginBottom: px(margin),
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
}),
|
|
439
|
+
!preserveHalfLeading
|
|
440
|
+
? {
|
|
441
|
+
cancelHalfLeadingPx: margin,
|
|
442
|
+
}
|
|
443
|
+
: {}
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const leadingCancel: CSSObject = {
|
|
448
|
+
display: 'block',
|
|
449
|
+
width: 0,
|
|
450
|
+
height: 0,
|
|
451
|
+
content: `''`,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function spacingProperty(
|
|
455
|
+
property: SpacingProperty,
|
|
456
|
+
direction: 'top' | 'right' | 'bottom' | 'left'
|
|
457
|
+
) {
|
|
458
|
+
return `${property}-${direction}`
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const createSpacingCss =
|
|
462
|
+
<T extends Theme>(theme: { spacing: T['spacing'] }) =>
|
|
463
|
+
(
|
|
464
|
+
property: SpacingProperty,
|
|
465
|
+
modifiers: readonly [SpacingDirection, keyof T['spacing'] | 'auto'][]
|
|
466
|
+
): Internal => {
|
|
467
|
+
const { top, right, bottom, left } = modifiers.reduce(
|
|
468
|
+
(acc, [direction, size]) => {
|
|
469
|
+
if (direction === 'all') {
|
|
470
|
+
acc.top = size
|
|
471
|
+
acc.right = size
|
|
472
|
+
acc.bottom = size
|
|
473
|
+
acc.left = size
|
|
474
|
+
} else if (direction === 'vertical') {
|
|
475
|
+
acc.top = size
|
|
476
|
+
acc.bottom = size
|
|
477
|
+
} else if (direction === 'horizontal') {
|
|
478
|
+
acc.right = size
|
|
479
|
+
acc.left = size
|
|
480
|
+
} else {
|
|
481
|
+
acc[direction] = size
|
|
482
|
+
}
|
|
483
|
+
return acc
|
|
484
|
+
},
|
|
485
|
+
{} as Partial<
|
|
486
|
+
Record<'top' | 'right' | 'bottom' | 'left', keyof T['spacing'] | 'auto'>
|
|
487
|
+
>
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
const hasVerticalPadding =
|
|
491
|
+
property === 'padding' &&
|
|
492
|
+
top !== undefined &&
|
|
493
|
+
bottom !== undefined &&
|
|
494
|
+
top !== 'auto' &&
|
|
495
|
+
bottom !== 'auto'
|
|
496
|
+
|
|
497
|
+
return internal(
|
|
498
|
+
({ cancelHalfLeadingPx = 0 }) => ({
|
|
499
|
+
...(top !== undefined && {
|
|
500
|
+
[spacingProperty(property, 'top')]:
|
|
501
|
+
top === 'auto'
|
|
502
|
+
? 'auto'
|
|
503
|
+
: px(
|
|
504
|
+
theme.spacing[top] +
|
|
505
|
+
(hasVerticalPadding ? cancelHalfLeadingPx : 0)
|
|
506
|
+
),
|
|
507
|
+
}),
|
|
508
|
+
...(bottom !== undefined && {
|
|
509
|
+
[spacingProperty(property, 'bottom')]:
|
|
510
|
+
bottom === 'auto'
|
|
511
|
+
? 'auto'
|
|
512
|
+
: px(
|
|
513
|
+
theme.spacing[bottom] +
|
|
514
|
+
(hasVerticalPadding ? cancelHalfLeadingPx : 0)
|
|
515
|
+
),
|
|
516
|
+
}),
|
|
517
|
+
...(right !== undefined && {
|
|
518
|
+
[spacingProperty(property, 'right')]:
|
|
519
|
+
right === 'auto' ? 'auto' : px(theme.spacing[right]),
|
|
520
|
+
}),
|
|
521
|
+
...(left !== undefined && {
|
|
522
|
+
[spacingProperty(property, 'left')]:
|
|
523
|
+
left === 'auto' ? 'auto' : px(theme.spacing[left]),
|
|
524
|
+
}),
|
|
525
|
+
}),
|
|
526
|
+
hasVerticalPadding ? { hasVerticalPadding: true } : {}
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const createFixedPxCss =
|
|
531
|
+
<T extends Theme>(theme: T) =>
|
|
532
|
+
(property: FixedProperty, size: keyof T['spacing'] | 'auto'): Internal =>
|
|
533
|
+
internal(() => ({
|
|
534
|
+
[property]: size === 'auto' ? 'auto' : px(theme.spacing[size]),
|
|
535
|
+
}))
|
|
536
|
+
|
|
537
|
+
const createFixedRelativeCss =
|
|
538
|
+
<T extends Theme>(_theme: T) =>
|
|
539
|
+
(property: FixedProperty, amount: '100%' | 'auto'): Internal =>
|
|
540
|
+
internal(() => ({
|
|
541
|
+
[property]: amount,
|
|
542
|
+
}))
|
|
543
|
+
|
|
544
|
+
const createFixedColumnCss =
|
|
545
|
+
<T extends Theme>(_theme: T) =>
|
|
546
|
+
(property: FixedProperty, span: number): Internal =>
|
|
547
|
+
internal(() => ({
|
|
548
|
+
[property]: px(columnPx(span)),
|
|
549
|
+
}))
|
|
550
|
+
|
|
551
|
+
const createElementEffectCss =
|
|
552
|
+
<T extends Theme, TElementEffect extends T['elementEffect']>(theme: {
|
|
553
|
+
elementEffect: TElementEffect
|
|
554
|
+
}) =>
|
|
555
|
+
(effects: readonly (keyof TElementEffect)[] = []): Internal =>
|
|
556
|
+
internal(() =>
|
|
557
|
+
effects.filter(isSupportedEffect).reduce<CSSObject>(
|
|
558
|
+
(acc, effect) => ({
|
|
559
|
+
...acc,
|
|
560
|
+
...onEffectPseudo(effect, {
|
|
561
|
+
opacity:
|
|
562
|
+
!(Array as ReadonlyArrayConstructor).isArray(
|
|
563
|
+
theme.elementEffect[effect]
|
|
564
|
+
) && theme.elementEffect[effect]?.type === 'opacity'
|
|
565
|
+
? theme.elementEffect[effect]?.opacity
|
|
566
|
+
: unreachable(),
|
|
567
|
+
}),
|
|
568
|
+
}),
|
|
569
|
+
{}
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
function borderProperty(direction: BorderDirection) {
|
|
574
|
+
return `border-${direction}`
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function borderShorthand(color: string) {
|
|
578
|
+
return `solid 1px ${color}`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const createBorderCss =
|
|
582
|
+
<T extends Theme>(theme: T) =>
|
|
583
|
+
(
|
|
584
|
+
variant: keyof T['border'],
|
|
585
|
+
directions: readonly BorderDirection[]
|
|
586
|
+
): Internal => {
|
|
587
|
+
const all = directions.length === 0
|
|
588
|
+
const value = borderShorthand(theme.border[variant].color)
|
|
589
|
+
return internal(() => ({
|
|
590
|
+
...(all
|
|
591
|
+
? { border: value }
|
|
592
|
+
: directions.reduce<CSSObject>(
|
|
593
|
+
(acc, direction) => ({
|
|
594
|
+
...acc,
|
|
595
|
+
[borderProperty(direction)]: value,
|
|
596
|
+
}),
|
|
597
|
+
{}
|
|
598
|
+
)),
|
|
599
|
+
}))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const createBorderRadiusCss =
|
|
603
|
+
<T extends Theme>(theme: T) =>
|
|
604
|
+
(size: keyof T['borderRadius']): Internal =>
|
|
605
|
+
internal(() => ({
|
|
606
|
+
borderRadius: px(theme.borderRadius[size]),
|
|
607
|
+
}))
|
|
608
|
+
|
|
609
|
+
interface Context {
|
|
610
|
+
cancelHalfLeadingPx?: number
|
|
611
|
+
hasVerticalPadding?: boolean
|
|
612
|
+
boxShadowTransition?: boolean
|
|
613
|
+
colorTransition?: boolean
|
|
614
|
+
backgroundColorTransition?: boolean
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const commonSpec = (_theme: unknown): Internal => {
|
|
618
|
+
const duration = dur(TRANSITION_DURATION)
|
|
619
|
+
const transition = (property: string[]) => ({
|
|
620
|
+
transition: property.map((v) => `${duration} ${v}`).join(', '),
|
|
621
|
+
})
|
|
622
|
+
return internal(
|
|
623
|
+
({
|
|
624
|
+
colorTransition = false,
|
|
625
|
+
backgroundColorTransition = false,
|
|
626
|
+
boxShadowTransition = false,
|
|
627
|
+
}) =>
|
|
628
|
+
transition(
|
|
629
|
+
[
|
|
630
|
+
colorTransition ? 'color' : null,
|
|
631
|
+
backgroundColorTransition ? 'background-color' : null,
|
|
632
|
+
boxShadowTransition ? 'box-shadow' : null,
|
|
633
|
+
].filter(isPresent)
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const internalSym: unique symbol = Symbol('internal')
|
|
639
|
+
|
|
640
|
+
function internal(
|
|
641
|
+
operation: (context: Context) => CSSObject,
|
|
642
|
+
context: Context = {}
|
|
643
|
+
): Internal {
|
|
644
|
+
return {
|
|
645
|
+
[internalSym]: {
|
|
646
|
+
operation,
|
|
647
|
+
context,
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export interface Internal {
|
|
653
|
+
[internalSym]: {
|
|
654
|
+
operation: (context: Context) => CSSObject
|
|
655
|
+
context: Context
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
type Blank = null | undefined | false
|
|
660
|
+
|
|
661
|
+
const nonBlank = <T>(value: T): value is T extends Blank ? never : T =>
|
|
662
|
+
isPresent(value) && (value as unknown) !== false
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* `theme(o => [...])` の `theme` ユーティリティを構築する
|
|
666
|
+
*
|
|
667
|
+
* @param _styled styled-componnets の `styled` そのもの (型推論のために用いられる)
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
*
|
|
671
|
+
* import styled from 'styled-components'
|
|
672
|
+
* const theme = createTheme(styled)
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
*
|
|
676
|
+
* const theme = createTheme<DefaultTheme>()
|
|
677
|
+
*/
|
|
678
|
+
function createTheme<T extends Theme>(_styled?: ThemedStyledInterface<T>) {
|
|
679
|
+
// `theme(o => [...])` の `o` の部分の型推論のためだけに使う意味のない変数
|
|
680
|
+
// Tを型変数のまま渡してcreateThemeが呼ばれるまで型の具象化が行われないようにする
|
|
681
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
682
|
+
const _phantomBuilder = builder<T>({} as any, true)
|
|
683
|
+
// ランタイムの `theme(o => [...])` のインターフェースを構築する
|
|
684
|
+
return (
|
|
685
|
+
// ユーザー定義
|
|
686
|
+
spec: (
|
|
687
|
+
o: typeof _phantomBuilder
|
|
688
|
+
) => Blank | Internal | (Blank | Internal)[]
|
|
689
|
+
): ThemeProp<T> =>
|
|
690
|
+
({ theme }) => {
|
|
691
|
+
// styled-componentsのランタイムから受け取ったthemeオブジェクトをbuilderに食わせて`o`をつくる
|
|
692
|
+
// さらに、ユーザー定義にbuilderが構築した`o`を食わせる
|
|
693
|
+
// (`o`を一時変数に入れてしまうと型Tの具象化が行われるので関数合成を優先する)
|
|
694
|
+
const rawSpecDescriptor = spec(builder(theme))
|
|
695
|
+
// ユーザー定義の配列を整形
|
|
696
|
+
const specDescriptor = [
|
|
697
|
+
...(Array.isArray(rawSpecDescriptor)
|
|
698
|
+
? rawSpecDescriptor
|
|
699
|
+
: [rawSpecDescriptor]),
|
|
700
|
+
commonSpec(theme),
|
|
701
|
+
].filter(nonBlank)
|
|
702
|
+
|
|
703
|
+
// 1パス目
|
|
704
|
+
// 全ユーザー定義を舐めて相互に影響し合う定義をチェックし、その結果(コンテキスト)を取得
|
|
705
|
+
const context = specDescriptor.reduce<Context>(
|
|
706
|
+
(acc, v) => ({ ...acc, ...v[internalSym].context }),
|
|
707
|
+
{}
|
|
708
|
+
)
|
|
709
|
+
// 2パス目
|
|
710
|
+
// コンテキストを見ながら最適化されたCSSを構築
|
|
711
|
+
return specDescriptor.map((v) => v[internalSym].operation(context))
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// default export を疑似esmにしないようにする hack
|
|
716
|
+
createTheme.light = light
|
|
717
|
+
createTheme.dark = dark
|
|
718
|
+
export default createTheme
|
|
719
|
+
|
|
720
|
+
export type ThemeProp<T> = ({ theme }: { theme: T }) => CSSObject | CSSObject[]
|