@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.
Files changed (126) hide show
  1. package/.github/workflows/ci-cd-main.yml +44 -0
  2. package/.github/workflows/ci-cd-pull-request.yml +39 -0
  3. package/.node-version +1 -0
  4. package/.storybook/main.ts +22 -0
  5. package/.storybook/preview.ts +15 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +2 -0
  8. package/biome.jsonc +65 -0
  9. package/lib/components/Avatar/Avatar.tsx +91 -0
  10. package/lib/components/Avatar/AvatarGroup.stories.ts +67 -0
  11. package/lib/components/Avatar/AvatarGroup.tsx +42 -0
  12. package/lib/components/Avatar/avatar.module.css +59 -0
  13. package/lib/components/Avatar/avatar.stories.tsx +44 -0
  14. package/lib/components/Avatar/avatarGroup.module.css +78 -0
  15. package/lib/components/Avatar/color.ts +71 -0
  16. package/lib/components/Avatar/index.ts +2 -0
  17. package/lib/components/Badge/Badge.tsx +19 -0
  18. package/lib/components/Badge/badge.module.css +36 -0
  19. package/lib/components/Badge/index.tsx +1 -0
  20. package/lib/components/Button/Button.stories.ts +44 -0
  21. package/lib/components/Button/Button.tsx +39 -0
  22. package/lib/components/Button/ButtonBase.tsx +53 -0
  23. package/lib/components/Button/ComboButton.stories.ts +45 -0
  24. package/lib/components/Button/ComboButton.tsx +44 -0
  25. package/lib/components/Button/button.module.css +82 -0
  26. package/lib/components/Button/buttonBase.module.css +77 -0
  27. package/lib/components/Button/comboButton.module.css +83 -0
  28. package/lib/components/Button/index.ts +3 -0
  29. package/lib/components/Header/DigdirLogomark.tsx +23 -0
  30. package/lib/components/Header/GlobalMenu.stories.tsx +202 -0
  31. package/lib/components/Header/GlobalMenu.tsx +131 -0
  32. package/lib/components/Header/Header.stories.ts +85 -0
  33. package/lib/components/Header/Header.tsx +64 -0
  34. package/lib/components/Header/HeaderBase.tsx +10 -0
  35. package/lib/components/Header/HeaderButton.stories.ts +54 -0
  36. package/lib/components/Header/HeaderButton.tsx +55 -0
  37. package/lib/components/Header/HeaderLogo.stories.ts +17 -0
  38. package/lib/components/Header/HeaderLogo.tsx +22 -0
  39. package/lib/components/Header/HeaderSearch.stories.ts +20 -0
  40. package/lib/components/Header/HeaderSearch.tsx +44 -0
  41. package/lib/components/Header/globalMenu.module.css +28 -0
  42. package/lib/components/Header/header.module.css +39 -0
  43. package/lib/components/Header/headerButton.module.css +35 -0
  44. package/lib/components/Header/headerLogo.module.css +24 -0
  45. package/lib/components/Header/headerSearch.module.css +30 -0
  46. package/lib/components/Header/index.tsx +5 -0
  47. package/lib/components/Icon/CheckboxIcon.stories.ts +25 -0
  48. package/lib/components/Icon/CheckboxIcon.tsx +29 -0
  49. package/lib/components/Icon/Icon.stories.ts +24 -0
  50. package/lib/components/Icon/Icon.tsx +23 -0
  51. package/lib/components/Icon/RadioIcon.stories.ts +25 -0
  52. package/lib/components/Icon/RadioIcon.tsx +29 -0
  53. package/lib/components/Icon/SvgIcon.tsx +18 -0
  54. package/lib/components/Icon/__AkselIcon.tsx +37 -0
  55. package/lib/components/Icon/checkboxIcon.module.css +21 -0
  56. package/lib/components/Icon/icon.module.css +4 -0
  57. package/lib/components/Icon/iconsMap.tsx +2078 -0
  58. package/lib/components/Icon/index.ts +5 -0
  59. package/lib/components/Icon/radioIcon.module.css +21 -0
  60. package/lib/components/Layout/Layout.stories.ts +127 -0
  61. package/lib/components/Layout/Layout.tsx +40 -0
  62. package/lib/components/Layout/LayoutBase.stories.ts +17 -0
  63. package/lib/components/Layout/LayoutBase.tsx +30 -0
  64. package/lib/components/Layout/LayoutBody.stories.ts +17 -0
  65. package/lib/components/Layout/LayoutBody.tsx +16 -0
  66. package/lib/components/Layout/LayoutContent.stories.ts +17 -0
  67. package/lib/components/Layout/LayoutContent.tsx +15 -0
  68. package/lib/components/Layout/LayoutSidebar.stories.ts +17 -0
  69. package/lib/components/Layout/LayoutSidebar.tsx +16 -0
  70. package/lib/components/Layout/index.tsx +4 -0
  71. package/lib/components/Layout/layout.module.css +63 -0
  72. package/lib/components/Menu/Menu.stories.ts +495 -0
  73. package/lib/components/Menu/Menu.tsx +123 -0
  74. package/lib/components/Menu/MenuBase.tsx +17 -0
  75. package/lib/components/Menu/MenuGroup.tsx +18 -0
  76. package/lib/components/Menu/MenuHeader.tsx +13 -0
  77. package/lib/components/Menu/MenuItem.stories.ts +127 -0
  78. package/lib/components/Menu/MenuItem.tsx +58 -0
  79. package/lib/components/Menu/MenuItemBase.tsx +62 -0
  80. package/lib/components/Menu/MenuItemLabel.tsx +30 -0
  81. package/lib/components/Menu/MenuItemMedia.tsx +42 -0
  82. package/lib/components/Menu/MenuOption.stories.ts +50 -0
  83. package/lib/components/Menu/MenuOption.tsx +45 -0
  84. package/lib/components/Menu/MenuSearch.stories.ts +18 -0
  85. package/lib/components/Menu/MenuSearch.tsx +25 -0
  86. package/lib/components/Menu/index.ts +10 -0
  87. package/lib/components/Menu/menu.module.css +26 -0
  88. package/lib/components/Menu/menuHeader.module.css +12 -0
  89. package/lib/components/Menu/menuItem.module.css +136 -0
  90. package/lib/components/Menu/menuOption.module.css +29 -0
  91. package/lib/components/Menu/menuSearch.module.css +29 -0
  92. package/lib/components/Menu/useClickOutside.ts +21 -0
  93. package/lib/components/Menu/useEscapeKey.ts +16 -0
  94. package/lib/components/Toolbar/Toolbar.stories.tsx +188 -0
  95. package/lib/components/Toolbar/Toolbar.tsx +138 -0
  96. package/lib/components/Toolbar/ToolbarAdd.stories.ts +25 -0
  97. package/lib/components/Toolbar/ToolbarAdd.tsx +25 -0
  98. package/lib/components/Toolbar/ToolbarBase.tsx +27 -0
  99. package/lib/components/Toolbar/ToolbarButton.stories.ts +32 -0
  100. package/lib/components/Toolbar/ToolbarButton.tsx +65 -0
  101. package/lib/components/Toolbar/ToolbarFilter.stories.ts +66 -0
  102. package/lib/components/Toolbar/ToolbarFilter.tsx +70 -0
  103. package/lib/components/Toolbar/ToolbarMenu.stories.ts +37 -0
  104. package/lib/components/Toolbar/ToolbarMenu.tsx +28 -0
  105. package/lib/components/Toolbar/ToolbarOptions.stories.ts +108 -0
  106. package/lib/components/Toolbar/ToolbarOptions.tsx +61 -0
  107. package/lib/components/Toolbar/ToolbarSearch.stories.ts +19 -0
  108. package/lib/components/Toolbar/ToolbarSearch.tsx +24 -0
  109. package/lib/components/Toolbar/index.js +3 -0
  110. package/lib/components/Toolbar/toolbar.module.css +43 -0
  111. package/lib/components/Toolbar/toolbarButton.module.css +3 -0
  112. package/lib/components/Toolbar/toolbarSearch.module.css +28 -0
  113. package/lib/components/index.ts +1 -0
  114. package/lib/css/colors.css +113 -0
  115. package/lib/css/global.css +12 -0
  116. package/lib/css/theme-company.css +15 -0
  117. package/lib/css/theme-global.css +15 -0
  118. package/lib/css/theme-neutral.css +15 -0
  119. package/lib/css/theme-person.css +15 -0
  120. package/lib/css/theme.css +24 -0
  121. package/lib/index.ts +1 -0
  122. package/package.json +52 -0
  123. package/tsconfig.json +23 -0
  124. package/tsconfig.node.json +11 -0
  125. package/typings.d.ts +1 -0
  126. 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
@@ -0,0 +1,2 @@
1
+ # altinn-components
2
+ Reusable react components
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,2 @@
1
+ export * from './Avatar';
2
+ export * from './AvatarGroup';
@@ -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
+ };