@altinn/altinn-components 0.0.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/.github/workflows/ci-cd-main.yml +44 -0
- package/.github/workflows/ci-cd-pull-request.yml +39 -0
- package/.node-version +1 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.ts +15 -0
- package/CHANGELOG.md +13 -0
- package/README.md +2 -0
- package/biome.jsonc +65 -0
- package/lib/components/Avatar/Avatar.tsx +91 -0
- package/lib/components/Avatar/AvatarGroup.stories.ts +67 -0
- package/lib/components/Avatar/AvatarGroup.tsx +42 -0
- package/lib/components/Avatar/avatar.module.css +59 -0
- package/lib/components/Avatar/avatar.stories.tsx +44 -0
- package/lib/components/Avatar/avatarGroup.module.css +78 -0
- package/lib/components/Avatar/color.ts +71 -0
- package/lib/components/Avatar/index.ts +2 -0
- package/lib/components/Badge/Badge.tsx +19 -0
- package/lib/components/Badge/badge.module.css +36 -0
- package/lib/components/Badge/index.tsx +1 -0
- package/lib/components/Button/Button.stories.ts +44 -0
- package/lib/components/Button/Button.tsx +39 -0
- package/lib/components/Button/ButtonBase.tsx +53 -0
- package/lib/components/Button/ComboButton.stories.ts +45 -0
- package/lib/components/Button/ComboButton.tsx +44 -0
- package/lib/components/Button/button.module.css +82 -0
- package/lib/components/Button/buttonBase.module.css +77 -0
- package/lib/components/Button/comboButton.module.css +83 -0
- package/lib/components/Button/index.ts +3 -0
- package/lib/components/Header/DigdirLogomark.tsx +23 -0
- package/lib/components/Header/GlobalMenu.stories.tsx +202 -0
- package/lib/components/Header/GlobalMenu.tsx +131 -0
- package/lib/components/Header/Header.stories.ts +85 -0
- package/lib/components/Header/Header.tsx +64 -0
- package/lib/components/Header/HeaderBase.tsx +10 -0
- package/lib/components/Header/HeaderButton.stories.ts +54 -0
- package/lib/components/Header/HeaderButton.tsx +55 -0
- package/lib/components/Header/HeaderLogo.stories.ts +17 -0
- package/lib/components/Header/HeaderLogo.tsx +22 -0
- package/lib/components/Header/HeaderSearch.stories.ts +20 -0
- package/lib/components/Header/HeaderSearch.tsx +44 -0
- package/lib/components/Header/globalMenu.module.css +28 -0
- package/lib/components/Header/header.module.css +39 -0
- package/lib/components/Header/headerButton.module.css +35 -0
- package/lib/components/Header/headerLogo.module.css +24 -0
- package/lib/components/Header/headerSearch.module.css +30 -0
- package/lib/components/Header/index.tsx +5 -0
- package/lib/components/Icon/CheckboxIcon.stories.ts +25 -0
- package/lib/components/Icon/CheckboxIcon.tsx +29 -0
- package/lib/components/Icon/Icon.stories.ts +24 -0
- package/lib/components/Icon/Icon.tsx +23 -0
- package/lib/components/Icon/RadioIcon.stories.ts +25 -0
- package/lib/components/Icon/RadioIcon.tsx +29 -0
- package/lib/components/Icon/SvgIcon.tsx +18 -0
- package/lib/components/Icon/__AkselIcon.tsx +37 -0
- package/lib/components/Icon/checkboxIcon.module.css +21 -0
- package/lib/components/Icon/icon.module.css +4 -0
- package/lib/components/Icon/iconsMap.tsx +2078 -0
- package/lib/components/Icon/index.ts +5 -0
- package/lib/components/Icon/radioIcon.module.css +21 -0
- package/lib/components/Layout/Layout.stories.ts +127 -0
- package/lib/components/Layout/Layout.tsx +40 -0
- package/lib/components/Layout/LayoutBase.stories.ts +17 -0
- package/lib/components/Layout/LayoutBase.tsx +30 -0
- package/lib/components/Layout/LayoutBody.stories.ts +17 -0
- package/lib/components/Layout/LayoutBody.tsx +16 -0
- package/lib/components/Layout/LayoutContent.stories.ts +17 -0
- package/lib/components/Layout/LayoutContent.tsx +15 -0
- package/lib/components/Layout/LayoutSidebar.stories.ts +17 -0
- package/lib/components/Layout/LayoutSidebar.tsx +16 -0
- package/lib/components/Layout/index.tsx +4 -0
- package/lib/components/Layout/layout.module.css +63 -0
- package/lib/components/Menu/Menu.stories.ts +495 -0
- package/lib/components/Menu/Menu.tsx +123 -0
- package/lib/components/Menu/MenuBase.tsx +17 -0
- package/lib/components/Menu/MenuGroup.tsx +18 -0
- package/lib/components/Menu/MenuHeader.tsx +13 -0
- package/lib/components/Menu/MenuItem.stories.ts +127 -0
- package/lib/components/Menu/MenuItem.tsx +58 -0
- package/lib/components/Menu/MenuItemBase.tsx +62 -0
- package/lib/components/Menu/MenuItemLabel.tsx +30 -0
- package/lib/components/Menu/MenuItemMedia.tsx +42 -0
- package/lib/components/Menu/MenuOption.stories.ts +50 -0
- package/lib/components/Menu/MenuOption.tsx +45 -0
- package/lib/components/Menu/MenuSearch.stories.ts +18 -0
- package/lib/components/Menu/MenuSearch.tsx +25 -0
- package/lib/components/Menu/index.ts +10 -0
- package/lib/components/Menu/menu.module.css +26 -0
- package/lib/components/Menu/menuHeader.module.css +12 -0
- package/lib/components/Menu/menuItem.module.css +136 -0
- package/lib/components/Menu/menuOption.module.css +29 -0
- package/lib/components/Menu/menuSearch.module.css +29 -0
- package/lib/components/Menu/useClickOutside.ts +21 -0
- package/lib/components/Menu/useEscapeKey.ts +16 -0
- package/lib/components/Toolbar/Toolbar.stories.tsx +188 -0
- package/lib/components/Toolbar/Toolbar.tsx +138 -0
- package/lib/components/Toolbar/ToolbarAdd.stories.ts +25 -0
- package/lib/components/Toolbar/ToolbarAdd.tsx +25 -0
- package/lib/components/Toolbar/ToolbarBase.tsx +27 -0
- package/lib/components/Toolbar/ToolbarButton.stories.ts +32 -0
- package/lib/components/Toolbar/ToolbarButton.tsx +65 -0
- package/lib/components/Toolbar/ToolbarFilter.stories.ts +66 -0
- package/lib/components/Toolbar/ToolbarFilter.tsx +70 -0
- package/lib/components/Toolbar/ToolbarMenu.stories.ts +37 -0
- package/lib/components/Toolbar/ToolbarMenu.tsx +28 -0
- package/lib/components/Toolbar/ToolbarOptions.stories.ts +108 -0
- package/lib/components/Toolbar/ToolbarOptions.tsx +61 -0
- package/lib/components/Toolbar/ToolbarSearch.stories.ts +19 -0
- package/lib/components/Toolbar/ToolbarSearch.tsx +24 -0
- package/lib/components/Toolbar/index.js +3 -0
- package/lib/components/Toolbar/toolbar.module.css +43 -0
- package/lib/components/Toolbar/toolbarButton.module.css +3 -0
- package/lib/components/Toolbar/toolbarSearch.module.css +28 -0
- package/lib/components/index.ts +1 -0
- package/lib/css/colors.css +113 -0
- package/lib/css/global.css +12 -0
- package/lib/css/theme-company.css +15 -0
- package/lib/css/theme-global.css +15 -0
- package/lib/css/theme-neutral.css +15 -0
- package/lib/css/theme-person.css +15 -0
- package/lib/css/theme.css +24 -0
- package/lib/index.ts +1 -0
- package/package.json +52 -0
- package/tsconfig.json +23 -0
- package/tsconfig.node.json +11 -0
- package/typings.d.ts +1 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
branches:
|
|
4
|
+
- main
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
pull-requests: write
|
|
9
|
+
|
|
10
|
+
name: release-please
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: googleapis/release-please-action@v4
|
|
17
|
+
id: release
|
|
18
|
+
env:
|
|
19
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
20
|
+
with:
|
|
21
|
+
release-type: node
|
|
22
|
+
# The logic below handles the npm publication:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
# these if statements ensure that a publication only occurs when
|
|
25
|
+
# a new release is created:
|
|
26
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version-file: .node-version
|
|
30
|
+
registry-url: 'https://registry.npmjs.org'
|
|
31
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
32
|
+
- name: Setup PNPM
|
|
33
|
+
uses: pnpm/action-setup@v2
|
|
34
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
35
|
+
with:
|
|
36
|
+
version: '9.12.3'
|
|
37
|
+
- run: pnpm install --frozen-lockfile
|
|
38
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
39
|
+
- run: pnpm build
|
|
40
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
41
|
+
- run: pnpm publish
|
|
42
|
+
env:
|
|
43
|
+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
44
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI/CD Pull Request
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
types: [opened, synchronize, reopened]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version-file: .node-version
|
|
20
|
+
|
|
21
|
+
- name: Setup PNPM
|
|
22
|
+
uses: pnpm/action-setup@v2
|
|
23
|
+
with:
|
|
24
|
+
version: '9.12.3'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: pnpm install --frozen-lockfile
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: pnpm biome check ./lib
|
|
31
|
+
|
|
32
|
+
- name: Type check
|
|
33
|
+
run: pnpm typecheck
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: pnpm build
|
|
37
|
+
|
|
38
|
+
- name: Build Storybook
|
|
39
|
+
run: pnpm build-storybook
|
package/.node-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
20.15.1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {StorybookConfig} from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: [
|
|
5
|
+
"../lib/components/**/*.stories.@(ts|tsx)",
|
|
6
|
+
],
|
|
7
|
+
addons: [
|
|
8
|
+
"@storybook/addon-onboarding",
|
|
9
|
+
"@storybook/addon-links",
|
|
10
|
+
"@storybook/addon-essentials",
|
|
11
|
+
"@chromatic-com/storybook",
|
|
12
|
+
"@storybook/addon-interactions",
|
|
13
|
+
],
|
|
14
|
+
framework: {
|
|
15
|
+
name: "@storybook/react-vite",
|
|
16
|
+
options: {},
|
|
17
|
+
},
|
|
18
|
+
docs: {
|
|
19
|
+
autodocs: "tag",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "../lib/css/global.css";
|
|
2
|
+
|
|
3
|
+
/** @type { import('@storybook/react').Preview } */
|
|
4
|
+
const preview = {
|
|
5
|
+
parameters: {
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/i,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default preview;
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1 (2024-10-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* setup project with components: Layout, Header, Toolbar, Menu and Avatar ([f789c56](https://github.com/Altinn/altinn-components/commit/f789c5600c76339b9d1d58b92405a1a202d9b702))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Miscellaneous Chores
|
|
12
|
+
|
|
13
|
+
* release 0.0.1 ([#8](https://github.com/Altinn/altinn-components/issues/8)) ([4a38885](https://github.com/Altinn/altinn-components/commit/4a3888565d53763307882f9fcc2cca503289c2c5))
|
package/README.md
ADDED
package/biome.jsonc
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"defaultBranch": "main"
|
|
7
|
+
},
|
|
8
|
+
"organizeImports": {
|
|
9
|
+
"enabled": true
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"formatWithErrors": true,
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"lineWidth": 120,
|
|
15
|
+
"lineEnding": "lf",
|
|
16
|
+
"ignore": [],
|
|
17
|
+
"indentStyle": "space",
|
|
18
|
+
"indentWidth": 2
|
|
19
|
+
},
|
|
20
|
+
"javascript": {
|
|
21
|
+
"formatter": {
|
|
22
|
+
"arrowParentheses": "always",
|
|
23
|
+
"quoteStyle": "single",
|
|
24
|
+
"jsxQuoteStyle": "double",
|
|
25
|
+
"semicolons": "always",
|
|
26
|
+
"trailingCommas": "all",
|
|
27
|
+
"quoteProperties": "asNeeded",
|
|
28
|
+
"bracketSpacing": true,
|
|
29
|
+
"bracketSameLine": false
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"css": {
|
|
33
|
+
"parser": {
|
|
34
|
+
"cssModules": true
|
|
35
|
+
},
|
|
36
|
+
"formatter": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"indentStyle": "space",
|
|
39
|
+
"indentWidth": 2
|
|
40
|
+
},
|
|
41
|
+
"linter": {
|
|
42
|
+
"enabled": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"linter": {
|
|
46
|
+
"enabled": true,
|
|
47
|
+
"rules": {
|
|
48
|
+
"suspicious": {
|
|
49
|
+
"noArrayIndexKey": "off"
|
|
50
|
+
},
|
|
51
|
+
"recommended": true,
|
|
52
|
+
"style": {
|
|
53
|
+
"noUnusedTemplateLiteral": "off",
|
|
54
|
+
"noNonNullAssertion": "off",
|
|
55
|
+
"useTemplate": "off"
|
|
56
|
+
},
|
|
57
|
+
"correctness": {
|
|
58
|
+
"useExhaustiveDependencies": "warn"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"files": {
|
|
63
|
+
"ignore": [".github", "node_modules", "dist", "build", ".storybook"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import styles from './avatar.module.css';
|
|
5
|
+
import { fromStringToColor } from './color';
|
|
6
|
+
|
|
7
|
+
export type AvatarType = 'company' | 'person' | 'custom';
|
|
8
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
export type AvatarVariant = 'square' | 'circle';
|
|
10
|
+
export type AvatarColor = 'dark' | 'light';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for the Avatar component.
|
|
14
|
+
*/
|
|
15
|
+
export interface AvatarProps {
|
|
16
|
+
/** The name to display in the avatar. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** The type of avatar. */
|
|
19
|
+
type?: AvatarType;
|
|
20
|
+
/** The size of the avatar. */
|
|
21
|
+
size?: AvatarSize;
|
|
22
|
+
/** The variant of the avatar shape. */
|
|
23
|
+
variant?: AvatarVariant;
|
|
24
|
+
/** The color theme of the avatar. */
|
|
25
|
+
color?: AvatarColor;
|
|
26
|
+
/** Additional class names to apply to the avatar. */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** URL of the image to display in the avatar. */
|
|
29
|
+
imageUrl?: string;
|
|
30
|
+
/** Alt text for the image. */
|
|
31
|
+
imageUrlAlt?: string;
|
|
32
|
+
/** Whether to display an outline around the avatar. */
|
|
33
|
+
outline?: boolean;
|
|
34
|
+
/** Custom label to display inside the avatar. */
|
|
35
|
+
customLabel?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Avatar component to display user or company avatars with various customization options.
|
|
40
|
+
*/
|
|
41
|
+
export const Avatar = ({
|
|
42
|
+
type,
|
|
43
|
+
size = 'sm',
|
|
44
|
+
variant,
|
|
45
|
+
color,
|
|
46
|
+
name = 'Avatar',
|
|
47
|
+
outline = false,
|
|
48
|
+
imageUrl,
|
|
49
|
+
imageUrlAlt,
|
|
50
|
+
customLabel,
|
|
51
|
+
className,
|
|
52
|
+
}: AvatarProps): JSX.Element => {
|
|
53
|
+
const [hasImageError, setHasImageError] = useState<boolean>(false);
|
|
54
|
+
|
|
55
|
+
const defaultVariant = type === 'person' ? 'circle' : 'square';
|
|
56
|
+
const defaultColor = type === 'person' ? 'light' : 'dark';
|
|
57
|
+
const appliedVariant = variant || defaultVariant;
|
|
58
|
+
const appliedColor = color || defaultColor;
|
|
59
|
+
|
|
60
|
+
const { backgroundColor, foregroundColor } = fromStringToColor(name, appliedColor);
|
|
61
|
+
const initials = (name[0] ?? '').toUpperCase();
|
|
62
|
+
const usingImageUrl = imageUrl && !hasImageError;
|
|
63
|
+
|
|
64
|
+
const inlineStyles = !usingImageUrl
|
|
65
|
+
? {
|
|
66
|
+
backgroundColor,
|
|
67
|
+
color: foregroundColor,
|
|
68
|
+
}
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={cx(styles.avatar, styles[appliedVariant], styles[size], { [styles.outline]: outline }, className)}
|
|
74
|
+
style={inlineStyles}
|
|
75
|
+
aria-hidden
|
|
76
|
+
>
|
|
77
|
+
{usingImageUrl ? (
|
|
78
|
+
<img
|
|
79
|
+
src={imageUrl}
|
|
80
|
+
className={styles.image}
|
|
81
|
+
alt={imageUrlAlt || imageUrl}
|
|
82
|
+
onError={() => {
|
|
83
|
+
setHasImageError(true);
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
) : (
|
|
87
|
+
<span>{customLabel || initials}</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import { AvatarGroup } from './AvatarGroup';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Avatar/AvatarGroup',
|
|
7
|
+
component: AvatarGroup,
|
|
8
|
+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
|
12
|
+
// layout: 'fullscreen',
|
|
13
|
+
},
|
|
14
|
+
args: {},
|
|
15
|
+
} satisfies Meta<typeof AvatarGroup>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const People: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
type: 'person',
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
name: 'Albert Åberg',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'Birger Meling',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Celine Dion',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Companies: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
type: 'company',
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
name: 'Albert Åberg',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Birger Meling',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Celine Dion',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const CompanyAndPerson: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
items: [
|
|
57
|
+
{
|
|
58
|
+
type: 'company',
|
|
59
|
+
name: 'Albert Åberg',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'person',
|
|
63
|
+
name: 'Birger Meling',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Avatar, type AvatarProps, type AvatarSize, type AvatarType } from '.';
|
|
4
|
+
import styles from './avatarGroup.module.css';
|
|
5
|
+
|
|
6
|
+
export interface AvatarGroupProps {
|
|
7
|
+
items?: AvatarProps[];
|
|
8
|
+
maxItemsCount?: number;
|
|
9
|
+
type?: AvatarType;
|
|
10
|
+
size?: AvatarSize;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AvatarGroup = ({ items = [], maxItemsCount = 4, type, size = 'sm', className }: AvatarGroupProps) => {
|
|
15
|
+
const maxItems = useMemo(() => items.slice(0, maxItemsCount), [items, maxItemsCount]);
|
|
16
|
+
|
|
17
|
+
if (items?.length === 0) {
|
|
18
|
+
return <div className={styles.avatarGroup} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ul className={cx(styles.reset, styles.group, styles[size], className)} data-count={maxItems?.length}>
|
|
23
|
+
{maxItems.map((avatar, index) => {
|
|
24
|
+
const lastLegalAvatarReached = index === maxItemsCount - 1;
|
|
25
|
+
const customLabel = avatar.customLabel || lastLegalAvatarReached ? items.length.toString() : undefined;
|
|
26
|
+
return (
|
|
27
|
+
<li className={cx(styles.reset, styles.item)} key={avatar.name}>
|
|
28
|
+
<Avatar
|
|
29
|
+
name={avatar.name}
|
|
30
|
+
customLabel={customLabel}
|
|
31
|
+
imageUrl={avatar.imageUrl}
|
|
32
|
+
imageUrlAlt={avatar.imageUrlAlt}
|
|
33
|
+
type={avatar?.type || type}
|
|
34
|
+
size={size}
|
|
35
|
+
outline
|
|
36
|
+
/>
|
|
37
|
+
</li>
|
|
38
|
+
);
|
|
39
|
+
})}
|
|
40
|
+
</ul>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.avatar {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: center;
|
|
4
|
+
align-items: center;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
--avatar-font-size-xs: 0.75rem;
|
|
7
|
+
--avatar-font-size-sm: 0.875rem;
|
|
8
|
+
--avatar-font-size-md: 1.125rem;
|
|
9
|
+
--avatar-font-size-lg: 1.25rem;
|
|
10
|
+
--avatar-font-size-xl: 1.5rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.circle {
|
|
14
|
+
border-radius: 50%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.square {
|
|
18
|
+
border-radius: 5%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.outline {
|
|
22
|
+
outline: 1px solid #ffffff;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.xs {
|
|
27
|
+
font-size: var(--avatar-font-size-xs);
|
|
28
|
+
width: 20px;
|
|
29
|
+
height: 20px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sm {
|
|
33
|
+
font-size: var(--avatar-font-size-sm);
|
|
34
|
+
width: 24px;
|
|
35
|
+
height: 24px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.md {
|
|
39
|
+
font-size: var(--avatar-font-size-md);
|
|
40
|
+
width: 30px;
|
|
41
|
+
height: 30px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.lg {
|
|
45
|
+
font-size: var(--avatar-font-size-lg);
|
|
46
|
+
width: 36px;
|
|
47
|
+
height: 36px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.xl {
|
|
51
|
+
font-size: var(--avatar-font-size-xl);
|
|
52
|
+
width: 44px;
|
|
53
|
+
height: 44px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.image {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Avatar } from './';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Avatar/Avatar',
|
|
6
|
+
component: Avatar,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {},
|
|
9
|
+
args: {
|
|
10
|
+
name: 'Jane Doe',
|
|
11
|
+
type: 'person',
|
|
12
|
+
variant: 'circle',
|
|
13
|
+
color: 'light',
|
|
14
|
+
size: 'xl',
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof Avatar>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
export const Person: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
type: 'person',
|
|
24
|
+
name: 'Jane Doe',
|
|
25
|
+
size: 'xl',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Company: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
type: 'company',
|
|
32
|
+
name: 'Boligeksperten',
|
|
33
|
+
variant: 'square',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Logo: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
variant: 'square',
|
|
40
|
+
imageUrl: 'https://avatars.githubusercontent.com/u/1536293?s=200&v=4',
|
|
41
|
+
size: 'xl',
|
|
42
|
+
color: 'dark',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.group {
|
|
2
|
+
display: flex;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.reset {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
text-indent: 0;
|
|
9
|
+
list-style-type: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.item {
|
|
13
|
+
position: relative;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.xs {
|
|
17
|
+
width: 32px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.xs[data-count="2"] * + * {
|
|
21
|
+
margin-left: -8px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.xs[data-count="3"] * + * {
|
|
25
|
+
margin-left: -14px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.xs[data-count="4"] * + * {
|
|
29
|
+
margin-left: -16px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sm {
|
|
33
|
+
width: 42px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sm[data-count="2"] * + * {
|
|
37
|
+
margin-left: -6px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sm[data-count="3"] * + * {
|
|
41
|
+
margin-left: -15px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sm[data-count="4"] * + * {
|
|
45
|
+
margin-left: -18px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.md {
|
|
49
|
+
width: 54px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.md[data-count="2"] * + * {
|
|
53
|
+
margin-left: -6px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.md[data-count="3"] * + * {
|
|
57
|
+
margin-left: -18px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.md[data-count="4"] * + * {
|
|
61
|
+
margin-left: -22px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.lg {
|
|
65
|
+
width: 66px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.lg[data-count="2"] * + * {
|
|
69
|
+
margin-left: -6px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.lg[data-count="3"] * + * {
|
|
73
|
+
margin-left: -21px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.lg[data-count="4"] * + * {
|
|
77
|
+
margin-left: -26px;
|
|
78
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type RGB = { r: number; g: number; b: number };
|
|
2
|
+
|
|
3
|
+
function generateHashCode(str: string): number {
|
|
4
|
+
return str.split('').reduce((hash, char) => {
|
|
5
|
+
return char.charCodeAt(0) + ((hash << 5) - hash);
|
|
6
|
+
}, 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hashCodeToHexColor(hash: number): string {
|
|
10
|
+
return (hash & 0x00ffffff).toString(16).toUpperCase().padStart(6, '0');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function luminance(hexCode: string): number {
|
|
14
|
+
const { r, g, b } = hexToRgb(hexCode);
|
|
15
|
+
const [lR, lG, lB] = [r, g, b].map((v) => {
|
|
16
|
+
const normalized = v / 255;
|
|
17
|
+
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
|
18
|
+
});
|
|
19
|
+
return lR * 0.2126 + lG * 0.7152 + lB * 0.0722;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hexToRgb(hex: string): RGB {
|
|
23
|
+
const bigint = Number.parseInt(hex, 16);
|
|
24
|
+
return {
|
|
25
|
+
r: (bigint >> 16) & 255,
|
|
26
|
+
g: (bigint >> 8) & 255,
|
|
27
|
+
b: bigint & 255,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
32
|
+
return ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function adjustToDarkerColor(hexCode: string): string {
|
|
36
|
+
const { r, g, b } = hexToRgb(hexCode);
|
|
37
|
+
const rNew = Math.floor(r * 0.5);
|
|
38
|
+
const gNew = Math.floor(g * 0.5);
|
|
39
|
+
const bNew = Math.floor(b * 0.5);
|
|
40
|
+
return rgbToHex(rNew, gNew, bNew);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function adjustToLighterColor(hexCode: string): string {
|
|
44
|
+
const { r, g, b } = hexToRgb(hexCode);
|
|
45
|
+
const rNew = Math.min(255, Math.floor(r + (255 - r) * 0.5));
|
|
46
|
+
const gNew = Math.min(255, Math.floor(g + (255 - g) * 0.5));
|
|
47
|
+
const bNew = Math.min(255, Math.floor(b + (255 - b) * 0.5));
|
|
48
|
+
return rgbToHex(rNew, gNew, bNew);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fromStringToColor(
|
|
52
|
+
name: string,
|
|
53
|
+
profile: 'light' | 'dark',
|
|
54
|
+
): { backgroundColor: string; foregroundColor: string } {
|
|
55
|
+
const hash = generateHashCode(name);
|
|
56
|
+
let hexColor = hashCodeToHexColor(hash);
|
|
57
|
+
const bgLuminance = luminance(hexColor);
|
|
58
|
+
|
|
59
|
+
if (profile === 'light' && bgLuminance <= 0.7) {
|
|
60
|
+
hexColor = adjustToLighterColor(hexColor);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (profile === 'dark' && bgLuminance > 0.1) {
|
|
64
|
+
hexColor = adjustToDarkerColor(hexColor);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
backgroundColor: `#${hexColor}`,
|
|
69
|
+
foregroundColor: profile === 'light' ? '#000000' : '#ffffff',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import styles from './badge.module.css';
|
|
4
|
+
|
|
5
|
+
interface BadgeProps {
|
|
6
|
+
label?: string | number;
|
|
7
|
+
variant?: 'neutral' | 'strong';
|
|
8
|
+
size?: 'medium' | 'small';
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// TODO: add aria-label to the badge
|
|
13
|
+
export const Badge = ({ label, variant = 'neutral', size = 'medium', children }: BadgeProps) => {
|
|
14
|
+
const classNames = cx(styles.badge, {
|
|
15
|
+
[styles.strong]: variant === 'strong',
|
|
16
|
+
[styles.small]: size === 'small',
|
|
17
|
+
});
|
|
18
|
+
return <span className={classNames}>{label || children}</span>;
|
|
19
|
+
};
|