Merged in feat/SW-3636-storybook-structure (pull request #3309)
feat(SW-3636): Storybook structure * New sections in Storybook sidebar * Group Storybook content files and add token files for spacing, border radius and shadows Approved-by: Joakim Jäderberg
This commit is contained in:
269
packages/design-system/.storybook/content/Colors.tsx
Normal file
269
packages/design-system/.storybook/content/Colors.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { useState } from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { kebabify } from '../../generate/utils'
|
||||
|
||||
export type ThemeValue = Record<'resolved' | 'alias', string | number>
|
||||
|
||||
export type Theme = Record<string, ThemeValue>
|
||||
|
||||
export type ThemeOption = {
|
||||
name: string
|
||||
displayName: string
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export type ColorsProps = {
|
||||
theme?: Theme
|
||||
themes?: ThemeOption[]
|
||||
defaultThemeName?: string
|
||||
}
|
||||
|
||||
import styles from './colors.module.css'
|
||||
|
||||
function getContrastColor(bgColor: string) {
|
||||
const r = parseInt(bgColor.substring(1, 3), 16)
|
||||
const g = parseInt(bgColor.substring(3, 5), 16)
|
||||
const b = parseInt(bgColor.substring(5, 7), 16)
|
||||
let a = parseInt(bgColor.substring(7, 9), 16)
|
||||
|
||||
if (isNaN(a)) {
|
||||
a = 255
|
||||
}
|
||||
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
if (luminance > 0.5) {
|
||||
return '#000'
|
||||
} else {
|
||||
if (a < 255 / 2) {
|
||||
return '#000'
|
||||
}
|
||||
return '#fff'
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(
|
||||
text: string,
|
||||
setCopied: (key: string) => void,
|
||||
clearCopied: () => void,
|
||||
key: string
|
||||
) {
|
||||
copy(text)
|
||||
setCopied(key)
|
||||
setTimeout(() => {
|
||||
clearCopied()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
export function Colors({
|
||||
theme: propTheme,
|
||||
themes,
|
||||
defaultThemeName,
|
||||
}: ColorsProps) {
|
||||
const [selectedThemeName, setSelectedThemeName] = useState<string>(
|
||||
defaultThemeName || themes?.[0]?.name || ''
|
||||
)
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null)
|
||||
|
||||
const currentTheme =
|
||||
propTheme ||
|
||||
themes?.find((t) => t.name === selectedThemeName)?.theme ||
|
||||
themes?.[0]?.theme
|
||||
|
||||
if (!currentTheme) {
|
||||
return <div>No theme available</div>
|
||||
}
|
||||
|
||||
const grouping: Record<string, Theme> = {}
|
||||
|
||||
for (const [k, v] of Object.entries(currentTheme)) {
|
||||
if (typeof v.resolved === 'string' && v.resolved.startsWith('#')) {
|
||||
const key = k.replace(/\/[^/]+$/, '')
|
||||
if (!grouping[key]) {
|
||||
grouping[key] = {}
|
||||
}
|
||||
|
||||
grouping[key][k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
{themes && themes.length > 0 && (
|
||||
<div className={styles.themeSelector}>
|
||||
<label htmlFor="theme-select" className={styles.themeLabel}>
|
||||
Theme:
|
||||
</label>
|
||||
<select
|
||||
id="theme-select"
|
||||
className={styles.themeSelect}
|
||||
value={selectedThemeName}
|
||||
onChange={(e) => setSelectedThemeName(e.target.value)}
|
||||
>
|
||||
{themes.map((t) => (
|
||||
<option key={t.name} value={t.name}>
|
||||
{t.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.jumpTo}>
|
||||
<label htmlFor="group-select" className={styles.groupLabel}>
|
||||
Jump to:
|
||||
</label>
|
||||
<select
|
||||
id="group-select"
|
||||
className={styles.groupSelect}
|
||||
onChange={(e) => {
|
||||
const el = document.getElementById(e.target.value)
|
||||
el?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option value="">- Select a grouping -</option>
|
||||
{Object.keys(grouping)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((title) => {
|
||||
return (
|
||||
<option key={title} value={kebabify(title)}>
|
||||
{title}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles.tip}>
|
||||
<span className={styles.tipIcon}>💡</span>
|
||||
Click any value to copy to clipboard
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.groups}>
|
||||
{Object.entries(grouping)
|
||||
.sort((a, b) => {
|
||||
return a[0].localeCompare(b[0])
|
||||
})
|
||||
.map(([title, values]) => {
|
||||
return (
|
||||
<div className={styles.group} key={title}>
|
||||
<h2 id={kebabify(title)} className={styles.title}>
|
||||
{title}
|
||||
</h2>
|
||||
<div className={styles.values}>
|
||||
{Object.entries(values).map(([k, v]) => {
|
||||
const tokenKey = `${title}-${k}`
|
||||
|
||||
return (
|
||||
<div className={styles.value} key={k}>
|
||||
<div className={styles.colorCard}>
|
||||
<div
|
||||
className={styles.colorSwatch}
|
||||
style={{
|
||||
color: getContrastColor(v.resolved.toString()),
|
||||
backgroundColor: v.resolved.toString(),
|
||||
}}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
`var(--${kebabify(k)})`,
|
||||
setCopiedKey,
|
||||
() => setCopiedKey(null),
|
||||
tokenKey
|
||||
)
|
||||
}}
|
||||
title="Click to copy CSS variable"
|
||||
>
|
||||
<div className={styles.colorValue}>
|
||||
{v.resolved.toString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.tokenInfo}>
|
||||
<div className={styles.tokenRow}>
|
||||
<span className={styles.tokenLabel}>Figma:</span>
|
||||
<code
|
||||
className={styles.tokenCode}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
k,
|
||||
setCopiedKey,
|
||||
() => setCopiedKey(null),
|
||||
tokenKey
|
||||
)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{k}
|
||||
</code>
|
||||
</div>
|
||||
<div className={styles.tokenRow}>
|
||||
<span className={styles.tokenLabel}>CSS:</span>
|
||||
<code
|
||||
className={styles.tokenCode}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
kebabify(k),
|
||||
setCopiedKey,
|
||||
() => setCopiedKey(null),
|
||||
tokenKey
|
||||
)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{kebabify(k)}
|
||||
</code>
|
||||
</div>
|
||||
{v.alias ? (
|
||||
<div className={styles.tokenRow}>
|
||||
<span className={styles.tokenLabel}>
|
||||
Alias:
|
||||
</span>
|
||||
<code
|
||||
className={styles.tokenCode}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
v.alias.toString(),
|
||||
setCopiedKey,
|
||||
() => setCopiedKey(null),
|
||||
tokenKey
|
||||
)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{v.alias}
|
||||
</code>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.tokenRow}>
|
||||
<span className={styles.tokenLabel}>Value:</span>
|
||||
<code
|
||||
className={styles.tokenCode}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
v.resolved.toString(),
|
||||
setCopiedKey,
|
||||
() => setCopiedKey(null),
|
||||
tokenKey
|
||||
)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{v.resolved}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
packages/design-system/.storybook/content/CornerRadius.mdx
Normal file
11
packages/design-system/.storybook/content/CornerRadius.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { CornerRadius } from './CornerRadius'
|
||||
|
||||
import { base } from '../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Corner Radius" />
|
||||
|
||||
# Corner Radius
|
||||
|
||||
<CornerRadius theme={base} />
|
||||
99
packages/design-system/.storybook/content/CornerRadius.tsx
Normal file
99
packages/design-system/.storybook/content/CornerRadius.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { kebabify } from '../../generate/utils'
|
||||
|
||||
import tableStyles from './tokens.module.css'
|
||||
|
||||
type ThemeValue = {
|
||||
resolved: string | number
|
||||
alias?: string | number
|
||||
}
|
||||
|
||||
type Theme = Record<string, ThemeValue>
|
||||
|
||||
type CornerRadiusProps = {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
copy(text)
|
||||
}
|
||||
|
||||
export function CornerRadius({ theme }: CornerRadiusProps) {
|
||||
// Filter corner radius tokens
|
||||
const cornerRadiusTokens: Theme = {}
|
||||
for (const [k, v] of Object.entries(theme)) {
|
||||
if (k.startsWith('Corner radius/')) {
|
||||
cornerRadiusTokens[k] = v as ThemeValue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by value
|
||||
const sortedTokens = Object.entries(cornerRadiusTokens).sort((a, b) => {
|
||||
const aValue = typeof a[1].resolved === 'number' ? a[1].resolved : 0
|
||||
const bValue = typeof b[1].resolved === 'number' ? b[1].resolved : 0
|
||||
return aValue - bValue
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={tableStyles.tableContainer}>
|
||||
<table className={tableStyles.table}>
|
||||
<thead className={tableStyles.tableHeader}>
|
||||
<tr>
|
||||
<th className={tableStyles.tableHeaderCell}>Token</th>
|
||||
<th className={tableStyles.tableHeaderCell}>Pixels</th>
|
||||
<th className={tableStyles.tableHeaderCell}>
|
||||
Visual representation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTokens.map(([k, v]) => {
|
||||
const value = typeof v.resolved === 'number' ? v.resolved : 0
|
||||
const valuePx = `${value}px`
|
||||
|
||||
return (
|
||||
<tr key={k} className={tableStyles.tableRow}>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.tokenName}
|
||||
onClick={() => {
|
||||
copyToClipboard(`var(--${kebabify(k)})`)
|
||||
}}
|
||||
title="Click to copy CSS variable"
|
||||
>
|
||||
{kebabify(k)}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.value}
|
||||
onClick={() => {
|
||||
copyToClipboard(valuePx)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{valuePx}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<div
|
||||
className={tableStyles.borderRadiusPreview}
|
||||
style={{
|
||||
borderRadius: `${value}px`,
|
||||
}}
|
||||
>
|
||||
{valuePx}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
packages/design-system/.storybook/content/Shadow.mdx
Normal file
11
packages/design-system/.storybook/content/Shadow.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Shadow } from './Shadow'
|
||||
|
||||
import { base } from '../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Shadow" />
|
||||
|
||||
# Shadow
|
||||
|
||||
<Shadow theme={base} />
|
||||
106
packages/design-system/.storybook/content/Shadow.tsx
Normal file
106
packages/design-system/.storybook/content/Shadow.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { kebabify } from '../../generate/utils'
|
||||
|
||||
import tableStyles from './tokens.module.css'
|
||||
|
||||
type ThemeValue = {
|
||||
resolved: string | number
|
||||
alias?: string | number
|
||||
}
|
||||
|
||||
type Theme = Record<string, ThemeValue>
|
||||
|
||||
type ShadowProps = {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
copy(text)
|
||||
}
|
||||
|
||||
export function Shadow({ theme }: ShadowProps) {
|
||||
// Filter shadow tokens
|
||||
const shadowTokens: Theme = {}
|
||||
for (const [k, v] of Object.entries(theme)) {
|
||||
if (k.startsWith('BoxShadow-')) {
|
||||
shadowTokens[k] = v as ThemeValue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by level
|
||||
const sortedTokens = Object.entries(shadowTokens).sort((a, b) => {
|
||||
const aLevel = parseInt(a[0].match(/\d+/)?.[0] || '0')
|
||||
const bLevel = parseInt(b[0].match(/\d+/)?.[0] || '0')
|
||||
return aLevel - bLevel
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={tableStyles.tableContainer}>
|
||||
<table className={tableStyles.table}>
|
||||
<thead className={tableStyles.tableHeader}>
|
||||
<tr>
|
||||
<th className={tableStyles.tableHeaderCell}>Token</th>
|
||||
<th className={tableStyles.tableHeaderCell}>Level</th>
|
||||
<th className={tableStyles.tableHeaderCell}>Value</th>
|
||||
<th className={tableStyles.tableHeaderCell}>
|
||||
Visual representation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTokens.map(([k, v]) => {
|
||||
const shadowValue =
|
||||
typeof v.resolved === 'string' ? v.resolved : ''
|
||||
const level = k.match(/\d+/)?.[0] || '0'
|
||||
|
||||
return (
|
||||
<tr key={k} className={tableStyles.tableRow}>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.tokenName}
|
||||
onClick={() => {
|
||||
copyToClipboard(`var(--${kebabify(k)})`)
|
||||
}}
|
||||
title="Click to copy CSS variable"
|
||||
>
|
||||
{kebabify(k)}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<span className={tableStyles.value}>Level {level}</span>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.value}
|
||||
onClick={() => {
|
||||
copyToClipboard(shadowValue)
|
||||
}}
|
||||
title="Click to copy"
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{shadowValue}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<div
|
||||
className={tableStyles.shadowPreview}
|
||||
style={{
|
||||
boxShadow: shadowValue,
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
packages/design-system/.storybook/content/Spacing.mdx
Normal file
11
packages/design-system/.storybook/content/Spacing.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Spacing } from './Spacing'
|
||||
|
||||
import { base } from '../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Spacing" />
|
||||
|
||||
# Spacing
|
||||
|
||||
<Spacing theme={base} />
|
||||
128
packages/design-system/.storybook/content/Spacing.tsx
Normal file
128
packages/design-system/.storybook/content/Spacing.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { kebabify } from '../../generate/utils'
|
||||
|
||||
import tableStyles from './tokens.module.css'
|
||||
|
||||
type ThemeValue = {
|
||||
resolved: string | number
|
||||
alias?: string | number
|
||||
}
|
||||
|
||||
type Theme = Record<string, ThemeValue>
|
||||
|
||||
type SpacingProps = {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
copy(text)
|
||||
}
|
||||
|
||||
// Extract base unit multiplier from token name
|
||||
function getBaseUnitMultiplier(tokenName: string): string {
|
||||
// Token names are like "Space/x0", "Space/x025", "Space/x05", "Space/x1", "Space/x15", etc.
|
||||
const match = tokenName.match(/Space\/x(\d+)/)
|
||||
if (!match) return '0x'
|
||||
|
||||
const num = match[1]
|
||||
|
||||
// Handle special cases
|
||||
if (num === '0') return '0x'
|
||||
if (num === '025') return '0.25x'
|
||||
if (num === '05') return '0.5x'
|
||||
if (num === '15') return '1.5x'
|
||||
|
||||
// For other numbers, they're already the multiplier (x1 = 1x, x2 = 2x, etc.)
|
||||
return `${num}x`
|
||||
}
|
||||
|
||||
export function Spacing({ theme }: SpacingProps) {
|
||||
// Filter spacing tokens
|
||||
const spacingTokens: Theme = {}
|
||||
for (const [k, v] of Object.entries(theme)) {
|
||||
if (k.startsWith('Space/')) {
|
||||
spacingTokens[k] = v as ThemeValue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by value
|
||||
const sortedTokens = Object.entries(spacingTokens).sort((a, b) => {
|
||||
const aValue = typeof a[1].resolved === 'number' ? a[1].resolved : 0
|
||||
const bValue = typeof b[1].resolved === 'number' ? b[1].resolved : 0
|
||||
return aValue - bValue
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={tableStyles.tableContainer}>
|
||||
<table className={tableStyles.table}>
|
||||
<thead className={tableStyles.tableHeader}>
|
||||
<tr>
|
||||
<th className={tableStyles.tableHeaderCell}>Token</th>
|
||||
<th className={tableStyles.tableHeaderCell}>
|
||||
Base unit multiplier
|
||||
</th>
|
||||
<th className={tableStyles.tableHeaderCell}>Pixels</th>
|
||||
<th className={tableStyles.tableHeaderCell}>
|
||||
Visual representation
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTokens.map(([k, v]) => {
|
||||
const value = typeof v.resolved === 'number' ? v.resolved : 0
|
||||
const valuePx = `${value}px`
|
||||
const multiplier = getBaseUnitMultiplier(k)
|
||||
|
||||
return (
|
||||
<tr key={k} className={tableStyles.tableRow}>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.tokenName}
|
||||
onClick={() => {
|
||||
copyToClipboard(`var(--${kebabify(k)})`)
|
||||
}}
|
||||
title="Click to copy CSS variable"
|
||||
>
|
||||
{kebabify(k)}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<span className={tableStyles.value}>{multiplier}</span>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<code
|
||||
className={tableStyles.value}
|
||||
onClick={() => {
|
||||
copyToClipboard(valuePx)
|
||||
}}
|
||||
title="Click to copy"
|
||||
>
|
||||
{valuePx}
|
||||
</code>
|
||||
</td>
|
||||
<td className={tableStyles.tableCell}>
|
||||
<div className={tableStyles.visualBarContainer}>
|
||||
<span className={tableStyles.visualBarLabel}>
|
||||
{valuePx}
|
||||
</span>
|
||||
<div
|
||||
className={tableStyles.visualBar}
|
||||
style={{
|
||||
width: `${value}px`,
|
||||
minWidth: value > 0 ? '2px' : '0',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { base } from '.'
|
||||
import { base } from '../../lib/tokens'
|
||||
|
||||
<Meta title="Global/Colors/Base" />
|
||||
<Meta title="Tokens/Colors/Base" />
|
||||
|
||||
# Colors: Base
|
||||
|
||||
274
packages/design-system/.storybook/content/colors.module.css
Normal file
274
packages/design-system/.storybook/content/colors.module.css
Normal file
@@ -0,0 +1,274 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
padding: 1.25rem 1.5rem;
|
||||
z-index: 100;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.themeSelector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.themeLabel,
|
||||
.groupLabel {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.themeSelect,
|
||||
.groupSelect {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
font-size: 0.875rem;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.themeSelect:hover,
|
||||
.groupSelect:hover {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.themeSelect:focus,
|
||||
.groupSelect:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.jumpTo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem !important;
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #fbbf24;
|
||||
color: #92400e;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tipIcon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
color: #111827;
|
||||
margin: 0 0 1.5rem 0;
|
||||
padding-top: 80px;
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
.values {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.colorCard {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.colorCard:hover {
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.colorSwatch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgba(0, 0, 0, var(--opacity)) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 0
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgba(0, 0, 0, var(--opacity)) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 0
|
||||
);
|
||||
background-position:
|
||||
0px 0,
|
||||
8px 8px;
|
||||
background-size:
|
||||
16px 16px,
|
||||
16px 16px;
|
||||
}
|
||||
|
||||
.colorSwatch:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.colorSwatch:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.colorValue {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(4px);
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.tokenInfo {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.tokenRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.tokenLabel {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
min-width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tokenCode {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
background: #f9fafb;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 6px;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tokenCode:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #d1d5db;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.tokenCode.tokenValue {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@keyframes copiedPulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.themeSelector,
|
||||
.jumpTo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themeSelect,
|
||||
.groupSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.values {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-top: 20px;
|
||||
scroll-margin-top: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { downtownCamper } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Downtown Camper" />
|
||||
|
||||
# Colors: Downtown Camper
|
||||
|
||||
<Colors theme={downtownCamper} />
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { grandHotel } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Grand Hotel" />
|
||||
|
||||
# Colors: Grand Hotel
|
||||
|
||||
<Colors theme={grandHotel} />
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { haymarket } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Haymarket" />
|
||||
|
||||
# Colors: Haymarket
|
||||
|
||||
<Colors theme={haymarket} />
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { hotelNorge } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Hotel Norge" />
|
||||
|
||||
# Colors: Hotel Norge
|
||||
|
||||
<Colors theme={hotelNorge} />
|
||||
40
packages/design-system/.storybook/content/colors/index.mdx
Normal file
40
packages/design-system/.storybook/content/colors/index.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import {
|
||||
base,
|
||||
scandic,
|
||||
scandicGo,
|
||||
downtownCamper,
|
||||
haymarket,
|
||||
marski,
|
||||
hotelNorge,
|
||||
grandHotel,
|
||||
theDock,
|
||||
} from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors" />
|
||||
|
||||
# Colors
|
||||
|
||||
Select a theme to view all available color tokens. Click on any value to copy it to your clipboard.
|
||||
|
||||
<Colors
|
||||
themes={[
|
||||
{ name: 'base', displayName: 'Base', theme: base },
|
||||
{ name: 'scandic', displayName: 'Scandic', theme: scandic },
|
||||
{ name: 'scandicGo', displayName: 'Scandic Go', theme: scandicGo },
|
||||
{
|
||||
name: 'downtownCamper',
|
||||
displayName: 'Downtown Camper',
|
||||
theme: downtownCamper,
|
||||
},
|
||||
{ name: 'haymarket', displayName: 'Haymarket', theme: haymarket },
|
||||
{ name: 'marski', displayName: 'Marski', theme: marski },
|
||||
{ name: 'hotelNorge', displayName: 'Hotel Norge', theme: hotelNorge },
|
||||
{ name: 'grandHotel', displayName: 'Grand Hotel', theme: grandHotel },
|
||||
{ name: 'theDock', displayName: 'The Dock', theme: theDock },
|
||||
]}
|
||||
defaultThemeName="scandic"
|
||||
/>
|
||||
11
packages/design-system/.storybook/content/colors/marski.mdx
Normal file
11
packages/design-system/.storybook/content/colors/marski.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { marski } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Marski" />
|
||||
|
||||
# Colors: Marski
|
||||
|
||||
<Colors theme={marski} />
|
||||
11
packages/design-system/.storybook/content/colors/scandic.mdx
Normal file
11
packages/design-system/.storybook/content/colors/scandic.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { scandic } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Scandic" />
|
||||
|
||||
# Colors: Scandic
|
||||
|
||||
<Colors theme={scandic} />
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { scandicGo } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/Scandic Go" />
|
||||
|
||||
# Colors: Scandic Go
|
||||
|
||||
<Colors theme={scandicGo} />
|
||||
11
packages/design-system/.storybook/content/colors/theDock.mdx
Normal file
11
packages/design-system/.storybook/content/colors/theDock.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from '../Colors'
|
||||
|
||||
import { theDock } from '../../../lib/tokens'
|
||||
|
||||
<Meta title="Tokens/Colors/The Dock" />
|
||||
|
||||
# Colors: The Dock
|
||||
|
||||
<Colors theme={theDock} />
|
||||
@@ -2,25 +2,31 @@ import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# Scandic Hotels design system ✨
|
||||
# Scandic Hotels Design System ✨
|
||||
|
||||
## Components
|
||||
The Scandic Hotels Design System is a collection of components, patterns, and utilities that are used to build the Scandic Hotels websites and apps.
|
||||
|
||||
### File structure
|
||||
## Storybook structure
|
||||
|
||||
- Tokens
|
||||
- Core Components
|
||||
- Product Components
|
||||
- Patterns
|
||||
- Compositions
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
[Component]
|
||||
├── Compositions/ # A folder with Storybook stories to showcase compositions with other components
|
||||
├──── [CompositionX].stories.tsx # Storybook stories that showcase component compositions
|
||||
├── [component].module.css # The CSS for the component
|
||||
├── [Component].stories.tsx # Storybook stories for the component, without compositions
|
||||
├── [component].module.css # The CSS for the component
|
||||
├── [Component].stories.tsx # Storybook stories for the component
|
||||
├── [Component].tsx # The main component file
|
||||
├── index.tsx # Entrypoint for the component exports
|
||||
├── types.ts # TypeScript typings for the component
|
||||
└── variants.ts # Class Variance Authority configuration for variants of the component
|
||||
```
|
||||
|
||||
### Components
|
||||
## Components
|
||||
|
||||
Each component of the design system is defined in `lib/components`.
|
||||
|
||||
@@ -28,8 +34,10 @@ Each component has an `index.tsx` file that exports the component and its option
|
||||
|
||||
The components that are considered public API from a consumer standpoint **must** have Storybook stories that showcases and documents their use. It should at least contain one default story that showcases the component by itself in its default state. More stories are added to showcase other variants or usages of the component.
|
||||
|
||||
The typings for each components live in their respective `types.ts` file inside the component folder.
|
||||
|
||||
## Styling
|
||||
|
||||
Styling is done with CSS modules.
|
||||
|
||||
Variants are implemented with [Class Variance Authority](https://cva.style/).
|
||||
|
||||
The typings for each components live in their respective `types.ts` file inside the component folder.
|
||||
141
packages/design-system/.storybook/content/tokens.module.css
Normal file
141
packages/design-system/.storybook/content/tokens.module.css
Normal file
@@ -0,0 +1,141 @@
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
background: #f9fafb;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.tableHeaderCell {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.tableHeaderCell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tableRow:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.tableRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tableCell {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #111827;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tableCell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.tokenName {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.tokenName:hover {
|
||||
background: #e5e7eb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.value:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.visualBar {
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
min-width: 2px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.visualBarContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.visualBarLabel {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.shadowPreview {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.borderRadiusPreview {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #3b82f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tableContainer {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.tableHeaderCell,
|
||||
.tableCell {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import { mergeConfig } from 'vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: '@storybook/nextjs-vite',
|
||||
stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
stories: [
|
||||
'../lib/**/*.mdx',
|
||||
'../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
'./content/**/*.mdx',
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-themes',
|
||||
|
||||
@@ -60,7 +60,15 @@ const preview: Preview = {
|
||||
|
||||
options: {
|
||||
storySort: {
|
||||
order: ['Introduction', 'Global', 'Components', 'Compositions', '*'],
|
||||
order: [
|
||||
'Introduction',
|
||||
'Tokens',
|
||||
'Core Components',
|
||||
'Product Components',
|
||||
'Patterns',
|
||||
'Compositions',
|
||||
'*',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IconName } from '../Icons/iconName'
|
||||
import { Typography } from '../Typography'
|
||||
|
||||
const meta: Meta<typeof Accordion> = {
|
||||
title: 'Components/Accordion',
|
||||
title: 'Core Components/Accordion',
|
||||
component: Accordion,
|
||||
argTypes: {
|
||||
type: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||
import { expect, fn } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof Alert> = {
|
||||
title: 'Components/Alert',
|
||||
title: 'Core Components/Alert',
|
||||
component: Alert,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Avatar } from '.'
|
||||
import { config } from './variants'
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
title: 'Components/Avatar',
|
||||
title: 'Core Components/Avatar',
|
||||
component: Avatar,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { BackToTopButton } from '.'
|
||||
import { config as backToTopButtonConfig } from './variants'
|
||||
|
||||
const meta: Meta<typeof BackToTopButton> = {
|
||||
title: 'Components/BackToTopButton',
|
||||
title: 'Patterns/BackToTopButton',
|
||||
component: BackToTopButton,
|
||||
argTypes: {
|
||||
onPress: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Badge } from './Badge.tsx'
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: 'Components/Badge',
|
||||
title: 'Core Components/Badge',
|
||||
component: Badge,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fn } from 'storybook/test'
|
||||
import { BookingCodeChip } from './index'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/BookingCodeChip',
|
||||
title: 'Product Components/BookingCodeChip',
|
||||
component: BookingCodeChip,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from './Button'
|
||||
import { config as buttonConfig } from './variants'
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
title: 'Core Components/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
onPress: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { config as typographyConfig } from '../Typography/variants'
|
||||
|
||||
const meta: Meta<typeof ButtonLink> = {
|
||||
title: 'Components/ButtonLink',
|
||||
title: 'Core Components/ButtonLink',
|
||||
component: ButtonLink,
|
||||
argTypes: {
|
||||
onClick: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Card } from './Card.tsx'
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: 'Components/Card',
|
||||
title: 'Core Components/Card',
|
||||
component: Card,
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ChipButton } from './ChipButton.tsx'
|
||||
import { config as chipButtonConfig } from './variants'
|
||||
|
||||
const meta: Meta<typeof ChipButton> = {
|
||||
title: 'Components/Chip/ChipButton',
|
||||
title: 'Core Components/ChipButton',
|
||||
component: ChipButton,
|
||||
argTypes: {
|
||||
variant: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { ChipLink } from './ChipLink.tsx'
|
||||
|
||||
const meta: Meta<typeof ChipLink> = {
|
||||
title: 'Components/Chip/ChipLink',
|
||||
title: 'Core Components/ChipLink',
|
||||
component: ChipLink,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Divider } from './Divider'
|
||||
|
||||
const meta: Meta<typeof Divider> = {
|
||||
title: 'Components/Divider',
|
||||
title: 'Core Components/Divider',
|
||||
component: Divider,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const facilityMapping: Record<string, FacilityEnum> = Object.fromEntries(
|
||||
const colorOptions = Object.keys(iconVariantConfig.variants.color)
|
||||
|
||||
const meta: Meta<typeof FacilityToIcon> = {
|
||||
title: 'Components/Facility To Icon',
|
||||
title: 'Core Components/Facility To Icon',
|
||||
component: FacilityToIcon,
|
||||
argTypes: {
|
||||
id: {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
|
||||
import Checkbox from './index'
|
||||
import { FormDecorator } from '../../../../.storybook/decorators/FormDecorator'
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'Components/Form/Checkbox',
|
||||
component: Checkbox,
|
||||
decorators: [FormDecorator],
|
||||
args: { name: 'checkbox' },
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Checkbox>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { PaymentMethodEnum } from '@scandic-hotels/common/constants/paymentMetho
|
||||
import { FormDecorator } from '../../../../.storybook/decorators/FormDecorator'
|
||||
|
||||
const meta: Meta<typeof PaymentOptionsGroup> = {
|
||||
title: 'Components/Payment/PaymentOptionsGroup',
|
||||
title: 'Patterns/Form/Payment/PaymentOptionsGroup',
|
||||
component: PaymentOptionsGroup,
|
||||
decorators: [FormDecorator],
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PaymentMethodEnum } from '@scandic-hotels/common/constants/paymentMetho
|
||||
import { FormDecorator } from '../../../../.storybook/decorators/FormDecorator'
|
||||
|
||||
const meta: Meta<typeof SelectPaymentMethod> = {
|
||||
title: 'Components/Payment/SelectCreditCard',
|
||||
title: 'Patterns/Form/Payment/SelectCreditCard',
|
||||
component: SelectPaymentMethod,
|
||||
argTypes: {},
|
||||
decorators: [FormDecorator],
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from '../Button'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
|
||||
const meta: Meta<typeof HotelCard> = {
|
||||
title: 'Components/HotelCard',
|
||||
title: 'Product Components/HotelCard/HotelCard',
|
||||
component: HotelCard,
|
||||
argTypes: {
|
||||
state: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fn } from 'storybook/test'
|
||||
import { hotelPins } from '../../../Map/InteractiveMap/storybookData'
|
||||
|
||||
const meta: Meta<typeof StandaloneHotelCardDialog> = {
|
||||
title: 'Components/StandaloneHotelCardDialog',
|
||||
title: 'Product Components/HotelCard/StandaloneHotelCardDialog',
|
||||
component: StandaloneHotelCardDialog,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { fn } from 'storybook/test'
|
||||
import { Button } from '../Button'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { HotelInfoCard } from './index'
|
||||
|
||||
const meta: Meta<typeof HotelInfoCard> = {
|
||||
title: 'Components/HotelInfoCard',
|
||||
title: 'Product Components/HotelInfoCard',
|
||||
component: HotelInfoCard,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IconButton } from './IconButton'
|
||||
import { config } from './variants'
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: 'Components/IconButton',
|
||||
title: 'Core Components/IconButton',
|
||||
component: IconButton,
|
||||
argTypes: {
|
||||
onPress: {
|
||||
|
||||
@@ -7,7 +7,8 @@ import { HTMLAttributes } from 'react'
|
||||
import { getIconAriaProps } from '../utils'
|
||||
|
||||
export interface MaterialIconProps
|
||||
extends Pick<MaterialSymbolProps, 'size' | 'icon' | 'className' | 'style'>,
|
||||
extends
|
||||
Pick<MaterialSymbolProps, 'size' | 'icon' | 'className' | 'style'>,
|
||||
Omit<HTMLAttributes<HTMLSpanElement>, 'color' | 'id'>,
|
||||
VariantProps<typeof iconVariants> {
|
||||
isFilled?: boolean
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InfoBox, Props } from './InfoBox'
|
||||
import { IconName } from '../Icons/iconName'
|
||||
|
||||
const meta: Meta<typeof InfoBox> = {
|
||||
title: 'Components/InfoBox',
|
||||
title: 'Core Components/InfoBox',
|
||||
component: InfoBox,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
|
||||
@@ -20,7 +20,7 @@ const DEFAULT_ARGS = {
|
||||
}
|
||||
|
||||
const meta: Meta<typeof InfoCard> = {
|
||||
title: 'Components/InfoCard',
|
||||
title: 'Product Components/InfoCard',
|
||||
component: InfoCard,
|
||||
argTypes: {
|
||||
topTitle: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Input } from './Input'
|
||||
import { TextField } from 'react-aria-components'
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'Components/Input',
|
||||
title: 'Core Components/Input',
|
||||
// @ts-expect-error Input does not support this, but wrapping <TextField> does
|
||||
component: ({ isInvalid, ...props }) => (
|
||||
<TextField isInvalid={isInvalid}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { InputLabel } from './InputLabel'
|
||||
|
||||
const meta: Meta<typeof InputLabel> = {
|
||||
title: 'Components/InputLabel',
|
||||
title: 'Core Components/InputLabel',
|
||||
component: InputLabel,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TextField } from 'react-aria-components'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'Components/Input (New)',
|
||||
title: 'Core Components/Input (New)',
|
||||
// @ts-expect-error Input does not support this, but wrapping <TextField> does
|
||||
component: ({ isInvalid, validationState, ...props }) => (
|
||||
<TextField isInvalid={isInvalid} data-validation-state={validationState}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RTETypeEnum } from './types/rte/enums'
|
||||
import { RTEImageVaultNode, RTENode } from './types/rte/node'
|
||||
|
||||
const meta: Meta<typeof JsonToHtml> = {
|
||||
title: 'Components/JsonToHtml',
|
||||
title: 'Core Components/JsonToHtml',
|
||||
component: JsonToHtml,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconName } from '../Icons/iconName'
|
||||
import type { LinkListItemProps } from './LinkListItem'
|
||||
|
||||
const meta: Meta<typeof LinkList> = {
|
||||
title: 'Components/LinkList',
|
||||
title: 'Core Components/LinkList/LinkList',
|
||||
component: LinkList,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LinkListItem } from './index'
|
||||
import { IconName } from '../../Icons/iconName'
|
||||
|
||||
const meta: Meta<typeof LinkListItem> = {
|
||||
title: 'Components/LinkListItem',
|
||||
title: 'Core Components/LinkList/LinkListItem',
|
||||
component: LinkListItem,
|
||||
argTypes: {
|
||||
isExternal: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Loading } from './Loading'
|
||||
import { config } from './variants'
|
||||
|
||||
const meta: Meta<typeof Loading> = {
|
||||
title: 'Components/Loading',
|
||||
title: 'Patterns/Loading',
|
||||
component: Loading,
|
||||
argTypes: {
|
||||
type: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { LoadingSpinner } from './index'
|
||||
|
||||
const meta: Meta<typeof LoadingSpinner> = {
|
||||
title: 'Components/LoadingSpinner',
|
||||
title: 'Patterns/LoadingSpinner',
|
||||
component: LoadingSpinner,
|
||||
argTypes: {
|
||||
fullPage: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InteractiveMap } from '.'
|
||||
import { hotelPins } from './storybookData'
|
||||
|
||||
const meta: Meta<typeof InteractiveMap> = {
|
||||
title: 'Components/Map/Interactive Map',
|
||||
title: 'Patterns/Map/Interactive Map',
|
||||
component: InteractiveMap,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SignatureHotelEnum } from '@scandic-hotels/common/constants/signatureHo
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
const meta: Meta<typeof HotelMarkerByType> = {
|
||||
title: 'Components/Map/Hotel Marker By Type',
|
||||
title: 'Patterns/Map/Hotel Marker By Type',
|
||||
component: HotelMarkerByType,
|
||||
argTypes: {
|
||||
hotelType: {
|
||||
|
||||
@@ -5,7 +5,7 @@ type MessageBannerType = 'default' | 'error' | 'info'
|
||||
type TextColor = 'default' | 'error'
|
||||
|
||||
const meta: Meta<typeof MessageBanner> = {
|
||||
title: 'Components/MessageBanner',
|
||||
title: 'Core Components/MessageBanner',
|
||||
component: MessageBanner,
|
||||
argTypes: {
|
||||
type: {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
|
||||
import { expect } from 'storybook/test'
|
||||
import OldDSLink from '.'
|
||||
|
||||
const meta: Meta<typeof OldDSLink> = {
|
||||
title: 'Components/OldDSLink',
|
||||
component: OldDSLink,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'regular', 'tiny', 'none'],
|
||||
},
|
||||
scroll: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
prefetch: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
partialMatch: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof OldDSLink>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
active: false,
|
||||
href: 'https://www.scandichotels.com/en',
|
||||
},
|
||||
render: (args) => <OldDSLink {...args}>{args.href}</OldDSLink>,
|
||||
play: async ({ canvasElement }) => {
|
||||
const link = canvasElement.querySelector('a')
|
||||
if (!link) throw new Error('Link not found')
|
||||
expect(link).toBeInTheDocument()
|
||||
},
|
||||
}
|
||||
|
||||
export const Focused: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: Default.render,
|
||||
play: async ({ canvasElement }) => {
|
||||
const link = canvasElement.querySelector('a')
|
||||
if (!link) throw new Error('Link not found')
|
||||
expect(link).toBeInTheDocument()
|
||||
|
||||
expect(link).not.toHaveFocus()
|
||||
let styles = getComputedStyle(link)
|
||||
expect(styles.outlineStyle).toBe('none')
|
||||
expect(parseFloat(styles.outlineWidth)).toBe(0)
|
||||
|
||||
link?.focus()
|
||||
|
||||
expect(link).toHaveFocus()
|
||||
styles = getComputedStyle(link)
|
||||
expect(styles.outlineStyle).not.toBe('none')
|
||||
expect(parseFloat(styles.outlineWidth)).toBeGreaterThan(0)
|
||||
},
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { expect } from 'storybook/test'
|
||||
const methods = Object.values(PaymentMethodEnum).toSorted()
|
||||
|
||||
const meta: Meta<typeof PaymentMethodIcon> = {
|
||||
title: 'Components/Payment/PaymentMethodIcon',
|
||||
title: 'Product Components/Payment/PaymentMethodIcon',
|
||||
component: PaymentMethodIcon,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Progress } from './index'
|
||||
|
||||
const meta: Meta<typeof Progress> = {
|
||||
title: 'Components/Progress',
|
||||
title: 'Core Components/Progress',
|
||||
component: Progress,
|
||||
parameters: {
|
||||
backgrounds: { disable: true },
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import CampaignRateCard from '.'
|
||||
|
||||
const meta: Meta<typeof CampaignRateCard> = {
|
||||
title: 'Components/RateCard/Campaign',
|
||||
title: 'Product Components/RateCard/Campaign',
|
||||
component: CampaignRateCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import CodeRateCard from '.'
|
||||
|
||||
const meta: Meta<typeof CodeRateCard> = {
|
||||
title: 'Components/RateCard/Code',
|
||||
title: 'Product Components/RateCard/Code',
|
||||
component: CodeRateCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import NoRateAvailableCard from '.'
|
||||
|
||||
const meta: Meta<typeof NoRateAvailableCard> = {
|
||||
title: 'Components/RateCard/NoRateAvailable',
|
||||
title: 'Product Components/RateCard/NoRateAvailable',
|
||||
component: NoRateAvailableCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import PointsRateCard from '.'
|
||||
|
||||
const meta: Meta<typeof PointsRateCard> = {
|
||||
title: 'Components/RateCard/Points',
|
||||
title: 'Product Components/RateCard/Points',
|
||||
component: PointsRateCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import RegularRateCard from '.'
|
||||
|
||||
const meta: Meta<typeof RegularRateCard> = {
|
||||
title: 'Components/RateCard/Regular',
|
||||
title: 'Product Components/RateCard/Regular',
|
||||
component: RegularRateCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Select } from './Select'
|
||||
|
||||
const meta: Meta<typeof Select> = {
|
||||
title: 'Components/Select',
|
||||
title: 'Core Components/Select',
|
||||
component: Select,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Typography } from '../Typography'
|
||||
import { config } from './variants'
|
||||
|
||||
const meta: Meta<typeof TextLink> = {
|
||||
title: 'Components/TextLink',
|
||||
title: 'Core Components/TextLink',
|
||||
component: TextLink,
|
||||
argTypes: {
|
||||
theme: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { expect } from 'storybook/test'
|
||||
import { config } from './variants.ts'
|
||||
|
||||
const meta: Meta<typeof Toast> = {
|
||||
title: 'Components/Toasts/Toast',
|
||||
title: 'Core Components/Toast/Toast',
|
||||
component: Toast,
|
||||
argTypes: {
|
||||
variant: {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Button } from '../Button/Button.tsx'
|
||||
import { expect, waitFor } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof Toast> = {
|
||||
title: 'Components/Toasts/ToastHandler',
|
||||
title: 'Core Components/Toast/ToastHandler',
|
||||
component: Toast,
|
||||
argTypes: {
|
||||
variant: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { TripAdvisorChip } from './index'
|
||||
|
||||
const meta: Meta<typeof TripAdvisorChip> = {
|
||||
title: 'Components/TripAdvisorChip',
|
||||
title: 'Product Components/TripAdvisorChip',
|
||||
component: TripAdvisorChip,
|
||||
argTypes: {
|
||||
rating: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import TypographyDocs from './Typography.docs.mdx'
|
||||
import { config as typographyConfig } from './variants'
|
||||
|
||||
const meta: Meta<typeof Typography> = {
|
||||
title: 'Components/Typography',
|
||||
title: 'Core Components/Typography',
|
||||
component: Typography,
|
||||
args: { variant: typographyConfig.defaultVariants.variant },
|
||||
argTypes: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { VideoPlayer } from '.'
|
||||
import { config as videoPlayerConfig } from './variants'
|
||||
|
||||
const meta: Meta<typeof VideoPlayer> = {
|
||||
title: 'Components/🚧 VideoPlayer 🚧',
|
||||
title: 'Core Components/🚧 VideoPlayer 🚧',
|
||||
component: VideoPlayer,
|
||||
|
||||
parameters: {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { kebabify } from '../../generate/utils'
|
||||
|
||||
export type ThemeValue = Record<'resolved' | 'alias', string | number>
|
||||
|
||||
export type Theme = Record<string, ThemeValue>
|
||||
|
||||
export type ColorsProps = {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
import styles from './colors.module.css'
|
||||
|
||||
function getContrastColor(bgColor: string) {
|
||||
const r = parseInt(bgColor.substring(1, 3), 16)
|
||||
const g = parseInt(bgColor.substring(3, 5), 16)
|
||||
const b = parseInt(bgColor.substring(5, 7), 16)
|
||||
let a = parseInt(bgColor.substring(7, 9), 16)
|
||||
|
||||
if (isNaN(a)) {
|
||||
a = 255
|
||||
}
|
||||
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
if (luminance > 0.5) {
|
||||
return '#000'
|
||||
} else {
|
||||
if (a < 255 / 2) {
|
||||
return '#000'
|
||||
}
|
||||
return '#fff'
|
||||
}
|
||||
}
|
||||
|
||||
export function Colors({ theme }: ColorsProps) {
|
||||
const grouping: Record<string, Theme> = {}
|
||||
|
||||
for (const [k, v] of Object.entries(theme)) {
|
||||
if (typeof v.resolved === 'string' && v.resolved.startsWith('#')) {
|
||||
const key = k.replace(/\/[^/]+$/, '')
|
||||
if (!grouping[key]) {
|
||||
grouping[key] = {}
|
||||
}
|
||||
|
||||
grouping[key][k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.jumpTo}>
|
||||
<label>
|
||||
Jump to:
|
||||
<select
|
||||
onChange={(e) => {
|
||||
const el = document.getElementById(e.target.value)
|
||||
el?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option>- Select a grouping -</option>
|
||||
{Object.keys(grouping)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((title) => {
|
||||
return (
|
||||
<option key={title} value={kebabify(title)}>
|
||||
{title}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
<span className={styles.tip}>
|
||||
Click on any of the values to copy to clipboard!
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.groups}>
|
||||
{Object.entries(grouping)
|
||||
.sort((a, b) => {
|
||||
return a[0].localeCompare(b[0])
|
||||
})
|
||||
.map(([title, values]) => {
|
||||
return (
|
||||
<div className={styles.group} key={title}>
|
||||
<h2 id={kebabify(title)} className={styles.title}>
|
||||
{title}
|
||||
</h2>
|
||||
<div className={styles.values}>
|
||||
{Object.entries(values).map(([k, v]) => {
|
||||
return (
|
||||
<div className={styles.value} key={k}>
|
||||
<div className={styles.colorContainer}>
|
||||
<div
|
||||
className={styles.color}
|
||||
style={{
|
||||
color: getContrastColor(v.resolved.toString()),
|
||||
backgroundColor: v.resolved.toString(),
|
||||
}}
|
||||
onClick={() => {
|
||||
copy(`var(--${kebabify(k)})`)
|
||||
}}
|
||||
>
|
||||
var(--{kebabify(k)})
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.tokenName}
|
||||
onClick={() => {
|
||||
copy(k)
|
||||
}}
|
||||
>
|
||||
Figma: {k}
|
||||
</div>
|
||||
<div
|
||||
className={styles.tokenName}
|
||||
onClick={() => {
|
||||
copy(kebabify(k))
|
||||
}}
|
||||
>
|
||||
CSS: {kebabify(k)}
|
||||
</div>
|
||||
{v.alias ? (
|
||||
<div
|
||||
className={styles.tokenAlias}
|
||||
onClick={() => {
|
||||
copy(v.alias.toString())
|
||||
}}
|
||||
>
|
||||
Alias: {v.alias}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={styles.tokenValue}
|
||||
onClick={() => {
|
||||
copy(v.resolved.toString())
|
||||
}}
|
||||
>
|
||||
Value: {v.resolved}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.jumpTo {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
padding: 0.5em;
|
||||
z-index: 100;
|
||||
border: solid 1px #ccc;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.jumpTo select {
|
||||
margin-left: 1em;
|
||||
padding: 0.3em;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px !important;
|
||||
background: #fffcdd;
|
||||
padding: 0.5em;
|
||||
border-radius: 4px;
|
||||
border: solid 1px #e8db45;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
padding-top: 60px !important; /* jumpTo element height:ish */
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
box-sizing: border-box;
|
||||
width: 50%;
|
||||
min-width: 500px;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.colorContainer {
|
||||
--opacity: 0.1;
|
||||
background:
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgba(0, 0, 0, var(--opacity)) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 0
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgba(0, 0, 0, var(--opacity)) 25%,
|
||||
transparent 25%,
|
||||
transparent 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 75%,
|
||||
rgba(0, 0, 0, var(--opacity)) 0
|
||||
),
|
||||
white;
|
||||
background-position:
|
||||
0px 0,
|
||||
5px 5px;
|
||||
background-size:
|
||||
10px 10px,
|
||||
10px 10px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
transition: transform 0.2s ease-out;
|
||||
margin-bottom: 1em;
|
||||
cursor: pointer;
|
||||
border: solid 1px #d9d9d9;
|
||||
}
|
||||
|
||||
.color {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 4em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.colorContainer:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.colorContainer:active {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.tokenName,
|
||||
.tokenValue,
|
||||
.tokenAlias {
|
||||
cursor: pointer;
|
||||
padding: 0.1em 0.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tokenName:hover,
|
||||
.tokenValue:hover,
|
||||
.tokenAlias:hover {
|
||||
transform-origin: left;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tokenName:active,
|
||||
.tokenValue:active,
|
||||
.tokenAlias:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { downtownCamper } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Downtown Camper" />
|
||||
|
||||
# Colors: Downtown Camper
|
||||
|
||||
<Colors theme={downtownCamper} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { grandHotel } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Grand Hotel" />
|
||||
|
||||
# Colors: Grand Hotel
|
||||
|
||||
<Colors theme={grandHotel} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { haymarket } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Haymarket" />
|
||||
|
||||
# Colors: Haymarket
|
||||
|
||||
<Colors theme={haymarket} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { hotelNorge } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Hotel Norge" />
|
||||
|
||||
# Colors: Hotel Norge
|
||||
|
||||
<Colors theme={hotelNorge} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { marski } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Marski" />
|
||||
|
||||
# Colors: Marski
|
||||
|
||||
<Colors theme={marski} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { scandic } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Scandic" />
|
||||
|
||||
# Colors: Scandic
|
||||
|
||||
<Colors theme={scandic} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { scandicGo } from '.'
|
||||
|
||||
<Meta title="Global/Colors/Scandic Go" />
|
||||
|
||||
# Colors: Scandic Go
|
||||
|
||||
<Colors theme={scandicGo} />
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs/blocks'
|
||||
|
||||
import { Colors } from './Colors'
|
||||
|
||||
import { theDock } from '.'
|
||||
|
||||
<Meta title="Global/Colors/The Dock" />
|
||||
|
||||
# Colors: The Dock
|
||||
|
||||
<Colors theme={theDock} />
|
||||
@@ -5,6 +5,12 @@
|
||||
"noEmit": true,
|
||||
"paths": {}
|
||||
},
|
||||
"include": ["example", "lib"],
|
||||
"include": [
|
||||
"example",
|
||||
"lib",
|
||||
".storybook/content/**/*.mdx",
|
||||
".storybook/content/**/*.tsx",
|
||||
"lib/tokens/index.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ export default defineConfig({
|
||||
'**/*.css',
|
||||
'**/*.test.ts',
|
||||
'**/*.stories.tsx',
|
||||
'lib/tokens/*.mdx',
|
||||
'lib/tokens/*.css',
|
||||
'lib/tokens/*.tsx',
|
||||
'.storybook/content/*.mdx',
|
||||
'.storybook/content/tokens/**/*.mdx',
|
||||
'.storybook/content/tokens/**/*.css',
|
||||
'.storybook/content/tokens/**/*.tsx',
|
||||
],
|
||||
// rollupTypes: true,
|
||||
bundledPackages: ['class-variance-authority', 'clsx'],
|
||||
|
||||
Reference in New Issue
Block a user