@meridian-ui/meridian 1.0.0
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/README.md +63 -0
- package/package.json +52 -0
- package/postcss.config.mjs +5 -0
- package/rollup.config.js +51 -0
- package/src/assets/add-tab.svg +4 -0
- package/src/assets/chevron-right.svg +4 -0
- package/src/assets/delete-tab.svg +3 -0
- package/src/assets/dummy-data/skeleton.json +42 -0
- package/src/assets/dummy-data/skeleton.ts +28 -0
- package/src/assets/meridian-toggle.svg +4 -0
- package/src/components/attributes/attribute-price.tsx +17 -0
- package/src/components/detail-views/detail-basic.tsx +121 -0
- package/src/components/detail-views/detail-view.scss +187 -0
- package/src/components/item-views/item-compact.tsx +72 -0
- package/src/components/item-views/item-pin.tsx +131 -0
- package/src/components/item-views/item-profile.tsx +140 -0
- package/src/components/item-views/item-vertical.tsx +145 -0
- package/src/components/item-views/item-view.scss +277 -0
- package/src/components/malleability/console/console-setting.tsx +184 -0
- package/src/components/malleability/console/console-view.tsx +47 -0
- package/src/components/malleability/console/detail-view-component.tsx +262 -0
- package/src/components/malleability/console/malleability-component.tsx +104 -0
- package/src/components/malleability/console/malleability-console.scss +285 -0
- package/src/components/malleability/console/overview-component.tsx +174 -0
- package/src/components/malleability/malleability-content-toggle.tsx +32 -0
- package/src/components/malleability/malleability-overview-tabs.tsx +212 -0
- package/src/components/malleability/malleability-toolbar.tsx +15 -0
- package/src/components/malleability/malleability.scss +199 -0
- package/src/components/overviews/overivew-basic-table.tsx +127 -0
- package/src/components/overviews/overview-basic-grid.tsx +27 -0
- package/src/components/overviews/overview-basic-list.tsx +61 -0
- package/src/components/overviews/overview-basic-map.tsx +358 -0
- package/src/components/overviews/overview-basic.scss +88 -0
- package/src/components/ui/dropdown-menu.tsx +61 -0
- package/src/helpers/attribute-set.helper.ts +4 -0
- package/src/helpers/attribute.helper.ts +334 -0
- package/src/helpers/spec.helper.ts +92 -0
- package/src/helpers/utils.helper.ts +22 -0
- package/src/helpers/view.helper.ts +184 -0
- package/src/index.css +149 -0
- package/src/index.ts +1 -0
- package/src/renderer/attribute.scss +59 -0
- package/src/renderer/attribute.tsx +305 -0
- package/src/renderer/renderer.data-bind.ts +573 -0
- package/src/renderer/renderer.defaults.ts +194 -0
- package/src/renderer/renderer.denormalize.ts +273 -0
- package/src/renderer/renderer.filter.ts +211 -0
- package/src/renderer/renderer.props.ts +21 -0
- package/src/renderer/renderer.scss +72 -0
- package/src/renderer/renderer.tsx +450 -0
- package/src/renderer/wrapper.tsx +225 -0
- package/src/spec/spec.internal.ts +76 -0
- package/src/spec/spec.ts +195 -0
- package/src/store/odi-malleability.store.ts +337 -0
- package/src/store/odi-navigation.store.ts +44 -0
- package/src/store/odi.store.ts +210 -0
- package/tailwind.config.js +31 -0
- package/tsconfig.json +24 -0
- package/types/svg.d.ts +6 -0
- package/vercel.json +5 -0
- package/webpack.config.js +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Meridian: A Design Framework for Malleable Overview-Detail Interfaces
|
|
2
|
+
|
|
3
|
+
> Bryan Min and Haijun Xia. 2025. Meridian: A Design Framework for Malleable Overview-Detail Interfaces. In The 38th Annual ACM Symposium on User Interface Software and Technology (UIST ’25), September 28-October 1, 2025, Busan, Republic of Korea. ACM, New York, NY, USA, 13 pages. https://doi.org/10.1145/3746059.3747654
|
|
4
|
+
|
|
5
|
+
This repository contains the Meridian developer package and two applications: a gallery of three example real-world ODIs and a no-code website builder.
|
|
6
|
+
|
|
7
|
+
## Meridian Specification Types
|
|
8
|
+
|
|
9
|
+
The Meridian specification is a JSON-based language for describing overview-detail interfaces.
|
|
10
|
+
|
|
11
|
+
To view the full specification types, navigate to `src/spec/spec.ts`.
|
|
12
|
+
|
|
13
|
+
To view the full specification for the three real-world examples from Section 5, navigate to the following files:
|
|
14
|
+
|
|
15
|
+
- Example 1: `examples/gallery/src/views/d2-1/att.meridian.ts`
|
|
16
|
+
- Example 2: `examples/gallery/src/views/d2-2/soccer.meridian.ts`
|
|
17
|
+
- Example 3: `examples/gallery/src/views/d2-3/thesaurus.meridian.ts`
|
|
18
|
+
|
|
19
|
+
To view the hotels specification from the video figure, navigate to: `examples/gallery/src/views/hotels/hotels.meridian.ts`
|
|
20
|
+
|
|
21
|
+
## Gallery Website
|
|
22
|
+
|
|
23
|
+
First, in the root directory, run
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
npm link meridian
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
(`meridian` being the name of the root directory)
|
|
30
|
+
|
|
31
|
+
Then navigate to `examples/gallery`, and run:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npm link
|
|
35
|
+
npm install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To run the gallery app, run:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
npm run dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Website Builder Application
|
|
45
|
+
|
|
46
|
+
Similar to running the gallery website, first should run
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
npm link meridian
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then navigate to `examples/web-builder`, and run:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
npm link
|
|
56
|
+
npm install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To run the gallery app, run:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
npm run dev
|
|
63
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meridian-ui/meridian",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/cjs/index.js",
|
|
5
|
+
"module": "dist/esm/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && npm run build:css",
|
|
10
|
+
"build:css": "npx tailwindcss -i ./src/styles/index.css -o ./dist/meridian.css --minify"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@tailwindcss/postcss": "^4.0.15",
|
|
17
|
+
"@types/react-beautiful-dnd": "^13.1.8",
|
|
18
|
+
"@vis.gl/react-google-maps": "^1.x.x",
|
|
19
|
+
"dotenv": "^16.4.7",
|
|
20
|
+
"interactjs": "^1.10.27",
|
|
21
|
+
"react": "^18.x.x",
|
|
22
|
+
"react-beautiful-dnd": "^13.1.1",
|
|
23
|
+
"react-dom": "^18.x.x",
|
|
24
|
+
"sass": "^1.86.0",
|
|
25
|
+
"zustand": "^4.x.x"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@rollup/plugin-commonjs": "^28.0.0",
|
|
29
|
+
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
30
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
31
|
+
"@rollup/plugin-typescript": "^12.1.0",
|
|
32
|
+
"@svgr/webpack": "^8.1.0",
|
|
33
|
+
"@types/react": "^19.0.0",
|
|
34
|
+
"@types/react-dom": "^19.0.0",
|
|
35
|
+
"autoprefixer": "^10.4.21",
|
|
36
|
+
"postcss": "^8.5.3",
|
|
37
|
+
"rollup": "^4.24.0",
|
|
38
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
39
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
40
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
41
|
+
"storybook": "^8.3.5",
|
|
42
|
+
"tailwindcss": "^4.0.15",
|
|
43
|
+
"ts-loader": "^9.5.1",
|
|
44
|
+
"tslib": "^2.7.0",
|
|
45
|
+
"typescript": "^5.6.3",
|
|
46
|
+
"webpack": "^5.95.0",
|
|
47
|
+
"webpack-cli": "^5.1.4"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
3
|
+
import typescript from "@rollup/plugin-typescript";
|
|
4
|
+
import dts from "rollup-plugin-dts";
|
|
5
|
+
import terser from "@rollup/plugin-terser";
|
|
6
|
+
import peerDepsExternal from "rollup-plugin-peer-deps-external";
|
|
7
|
+
import postcss from "rollup-plugin-postcss";
|
|
8
|
+
import tailwindcss from 'tailwindcss';
|
|
9
|
+
import autoprefixer from 'autoprefixer';
|
|
10
|
+
|
|
11
|
+
const packageJson = require("./package.json");
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
{
|
|
15
|
+
input: "src/index.ts",
|
|
16
|
+
output: [
|
|
17
|
+
{
|
|
18
|
+
file: packageJson.main,
|
|
19
|
+
format: "cjs",
|
|
20
|
+
sourcemap: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
file: packageJson.module,
|
|
24
|
+
format: "esm",
|
|
25
|
+
sourcemap: true,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
plugins: [
|
|
29
|
+
peerDepsExternal(),
|
|
30
|
+
resolve(),
|
|
31
|
+
commonjs(),
|
|
32
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
33
|
+
postcss({
|
|
34
|
+
plugins: [
|
|
35
|
+
tailwindcss(),
|
|
36
|
+
autoprefixer(),
|
|
37
|
+
],
|
|
38
|
+
extract: 'src/styles/index.css',
|
|
39
|
+
modules: false,
|
|
40
|
+
inject: false,
|
|
41
|
+
}),
|
|
42
|
+
terser(),
|
|
43
|
+
],
|
|
44
|
+
external: ["react", "react-dom"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
input: "src/index.ts",
|
|
48
|
+
output: [{ file: "dist/types.d.ts", format: "es" }],
|
|
49
|
+
plugins: [dts.default()],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.46967 6.46967C0.176777 6.76256 0.176777 7.23744 0.46967 7.53033C0.762563 7.82322 1.23744 7.82322 1.53033 7.53033L3.99902 5.06164L6.46967 7.53228C6.76256 7.82518 7.23744 7.82518 7.53033 7.53228C7.82322 7.23939 7.82322 6.76452 7.53033 6.47162L5.05968 4.00098L7.53033 1.53033C7.82322 1.23744 7.82322 0.762563 7.53033 0.46967C7.23744 0.176777 6.76256 0.176776 6.46967 0.46967L3.99902 2.94032L1.53033 0.471623C1.23744 0.17873 0.762563 0.17873 0.46967 0.471623C0.176777 0.764517 0.176777 1.23939 0.46967 1.53228L2.93836 4.00098L0.46967 6.46967Z" fill="black"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "0",
|
|
4
|
+
"name": ""
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"id": "1",
|
|
8
|
+
"name": ""
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "2",
|
|
12
|
+
"name": ""
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "3",
|
|
16
|
+
"name": ""
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "4",
|
|
20
|
+
"name": ""
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "5",
|
|
24
|
+
"name": ""
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "6",
|
|
28
|
+
"name": ""
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "7",
|
|
32
|
+
"name": ""
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "8",
|
|
36
|
+
"name": ""
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "9",
|
|
40
|
+
"name": ""
|
|
41
|
+
}
|
|
42
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BindingItemType, DataBindingType, ODI } from "../../spec/spec";
|
|
2
|
+
import { FetchedDataBindingType, FetchedODI } from "../../spec/spec.internal"
|
|
3
|
+
|
|
4
|
+
// const
|
|
5
|
+
|
|
6
|
+
// const skeletonBinding: BindingItemType = {
|
|
7
|
+
// id: '.id',
|
|
8
|
+
|
|
9
|
+
// }
|
|
10
|
+
|
|
11
|
+
const binding: BindingItemType = {
|
|
12
|
+
itemId: '.id',
|
|
13
|
+
attributes: [
|
|
14
|
+
{ value: '.name', roles: ['thumbnail'] },
|
|
15
|
+
{ value: '.name', roles: ['title'] },
|
|
16
|
+
{ value: '.name', roles: ['subtitle'] },
|
|
17
|
+
{ value: '.name', roles: ['description'] },
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dataSources: DataBindingType[] = [
|
|
22
|
+
{ binding: binding }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const skeletonODI: ODI = {
|
|
26
|
+
dataBinding: dataSources,
|
|
27
|
+
overviews: [{ type: 'list', itemView: { type: 'profile-skeleton'} }],
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="96" height="88" viewBox="0 0 96 88" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M38 34L38 28L32 28L16 28C10.4772 28 6 32.4771 6 38L6 72C6 77.5228 10.4771 82 16 82L28 82C33.5228 82 38 77.5228 38 72L38 34Z" stroke="black" stroke-width="12"/>
|
|
3
|
+
<path d="M58 54L58 60L64 60L80 60C85.5229 60 90 55.5228 90 50L90 16C90 10.4771 85.5228 6 80 6L68 6C62.4772 6 58 10.4772 58 16L58 54Z" stroke="black" stroke-width="12"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FetchedAttributeType } from '../../spec/spec.internal';
|
|
2
|
+
import { isAttributeType } from '../../helpers/spec.helper';
|
|
3
|
+
|
|
4
|
+
export const AttributePrice = ({
|
|
5
|
+
attribute,
|
|
6
|
+
}: {
|
|
7
|
+
attribute: FetchedAttributeType;
|
|
8
|
+
}) => {
|
|
9
|
+
if (!attribute || attribute.type !== 'price') return <></>;
|
|
10
|
+
|
|
11
|
+
// Check if attribute value is of the correct type
|
|
12
|
+
if (isAttributeType(attribute)) {
|
|
13
|
+
return <div>${attribute.value}</div>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return <></>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAttributesByRole,
|
|
3
|
+
getAttributesWithoutRoles,
|
|
4
|
+
} from '../../helpers/attribute.helper';
|
|
5
|
+
import { DetailViewConfig, OpenViewIn, Role } from '../../spec/spec';
|
|
6
|
+
import { FetchedItemType } from '../../spec/spec.internal';
|
|
7
|
+
import { useODI } from '../../store/odi.store';
|
|
8
|
+
import { Attribute } from '../../renderer/attribute';
|
|
9
|
+
import './detail-view.scss';
|
|
10
|
+
|
|
11
|
+
export interface DetailBasic extends DetailViewConfig {
|
|
12
|
+
type: 'basic';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DetailBasic = ({
|
|
16
|
+
// details,
|
|
17
|
+
item,
|
|
18
|
+
}: {
|
|
19
|
+
// details: Detail[];
|
|
20
|
+
item: FetchedItemType | undefined;
|
|
21
|
+
}) => {
|
|
22
|
+
const { odi, selectedItemEntity } = useODI();
|
|
23
|
+
|
|
24
|
+
if (!selectedItemEntity || !item) {
|
|
25
|
+
return <div></div>;
|
|
26
|
+
} else {
|
|
27
|
+
const unlabeledAttributes = getAttributesWithoutRoles(item, true) ?? [];
|
|
28
|
+
|
|
29
|
+
// console.log(item, getAttributesByRole(item, 'thumbnail'));
|
|
30
|
+
// console.log(odi, item);
|
|
31
|
+
return (
|
|
32
|
+
<div key={item.itemId} className="detail-view">
|
|
33
|
+
<div className="row g-3">
|
|
34
|
+
<div className="main-area">
|
|
35
|
+
<div className="thumbnail-area">
|
|
36
|
+
{/* Thumbnail */}
|
|
37
|
+
<Attribute
|
|
38
|
+
className="thumbnail"
|
|
39
|
+
options={selectedItemEntity.options}
|
|
40
|
+
attribute={getAttributesByRole(item, 'thumbnail')}
|
|
41
|
+
/>
|
|
42
|
+
{/* Caption */}
|
|
43
|
+
<Attribute
|
|
44
|
+
options={selectedItemEntity.options}
|
|
45
|
+
attribute={getAttributesByRole(item, 'caption')}
|
|
46
|
+
/>
|
|
47
|
+
{/* Badge */}
|
|
48
|
+
<Attribute
|
|
49
|
+
className="badge"
|
|
50
|
+
options={selectedItemEntity.options}
|
|
51
|
+
attribute={getAttributesByRole(item, 'badge')}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
{/* Main Content Section */}
|
|
55
|
+
<div className="content-area">
|
|
56
|
+
<div className="header-area">
|
|
57
|
+
{['title', 'subtitle', 'description', 'key-attribute'].map(
|
|
58
|
+
(role) =>
|
|
59
|
+
getAttributesByRole(item, role as Role) && (
|
|
60
|
+
<Attribute
|
|
61
|
+
key={role}
|
|
62
|
+
className={
|
|
63
|
+
role === 'title'
|
|
64
|
+
? 'title'
|
|
65
|
+
: role === 'subtitle'
|
|
66
|
+
? 'subtitle'
|
|
67
|
+
: ''
|
|
68
|
+
}
|
|
69
|
+
options={selectedItemEntity.options}
|
|
70
|
+
attribute={getAttributesByRole(item, role as Role)}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
{/* Tags */}
|
|
76
|
+
<div className="fl g-2 sm">
|
|
77
|
+
<Attribute
|
|
78
|
+
options={selectedItemEntity.options}
|
|
79
|
+
attribute={getAttributesByRole(item, 'tag')}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
{/* Action Links */}
|
|
83
|
+
<div className="fl g-2 sm">
|
|
84
|
+
<Attribute
|
|
85
|
+
options={selectedItemEntity.options}
|
|
86
|
+
attribute={getAttributesByRole(item, 'action')}
|
|
87
|
+
/>
|
|
88
|
+
<Attribute
|
|
89
|
+
options={selectedItemEntity.options}
|
|
90
|
+
attribute={getAttributesByRole(item, 'link')}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
{/* Specs */}
|
|
95
|
+
<Attribute
|
|
96
|
+
options={selectedItemEntity.options}
|
|
97
|
+
attribute={getAttributesByRole(item, 'spec')}
|
|
98
|
+
/>
|
|
99
|
+
{/* Footer */}
|
|
100
|
+
<Attribute
|
|
101
|
+
options={selectedItemEntity.options}
|
|
102
|
+
attribute={getAttributesByRole(item, 'footer')}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
{/* Unlabeled Attributes (Supplementary Content) */}
|
|
107
|
+
{unlabeledAttributes.length > 0 && (
|
|
108
|
+
<div className="fl g-2">
|
|
109
|
+
{unlabeledAttributes.map((attr, idx) => (
|
|
110
|
+
<Attribute
|
|
111
|
+
key={idx}
|
|
112
|
+
options={selectedItemEntity.options}
|
|
113
|
+
attribute={attr}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
.detail-view {
|
|
2
|
+
padding: 32px;
|
|
3
|
+
container-type: inline-size;
|
|
4
|
+
margin-left: auto;
|
|
5
|
+
margin-right: auto;
|
|
6
|
+
width: inherit;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: 12px;
|
|
10
|
+
align-items: flex-start;
|
|
11
|
+
padding: 32px;
|
|
12
|
+
|
|
13
|
+
@container (min-width: 768px) {
|
|
14
|
+
padding: 16px;
|
|
15
|
+
.main-area {
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
.thumbnail {
|
|
18
|
+
height: 480px;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.main-area {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: 16px;
|
|
27
|
+
|
|
28
|
+
.thumbnail-area {
|
|
29
|
+
position: relative;
|
|
30
|
+
width: 100%;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
gap: 16px;
|
|
35
|
+
.thumbnail {
|
|
36
|
+
max-width: 400px;
|
|
37
|
+
height: 200px;
|
|
38
|
+
border-radius: 16px;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.content-area {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 16px;
|
|
48
|
+
.header-area {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
.title {
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
}
|
|
55
|
+
.subtitle {
|
|
56
|
+
font-size: 16px;
|
|
57
|
+
color: #6b7280;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
.badge {
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: -16px;
|
|
66
|
+
left: -16px;
|
|
67
|
+
z-index: 100;
|
|
68
|
+
background-color: white;
|
|
69
|
+
border-radius: 12px;
|
|
70
|
+
padding: 2px 8px;
|
|
71
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.abs-float {
|
|
75
|
+
display: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
left: 0;
|
|
78
|
+
z-index: 70;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.fl {
|
|
82
|
+
display: flex;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.col {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
}
|
|
89
|
+
.row {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: row;
|
|
92
|
+
}
|
|
93
|
+
.j-c {
|
|
94
|
+
justify-content: center;
|
|
95
|
+
}
|
|
96
|
+
.i-c {
|
|
97
|
+
align-items: center;
|
|
98
|
+
}
|
|
99
|
+
.g-1 {
|
|
100
|
+
gap: 4px;
|
|
101
|
+
}
|
|
102
|
+
.g-2 {
|
|
103
|
+
gap: 8px;
|
|
104
|
+
}
|
|
105
|
+
.g-3 {
|
|
106
|
+
gap: 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.lg {
|
|
110
|
+
font-size: 18px;
|
|
111
|
+
}
|
|
112
|
+
.md {
|
|
113
|
+
font-size: 16px;
|
|
114
|
+
}
|
|
115
|
+
.sm {
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.f-m {
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
}
|
|
122
|
+
.f-b {
|
|
123
|
+
font-weight: 700;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
.item-profile {
|
|
129
|
+
position: relative;
|
|
130
|
+
display: flex;
|
|
131
|
+
gap: 16px;
|
|
132
|
+
padding: 8px;
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
.content-area {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 16px;
|
|
139
|
+
.header-area {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
gap: 4px;
|
|
143
|
+
.column {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
.title {
|
|
147
|
+
font-size: 18px;
|
|
148
|
+
}
|
|
149
|
+
.subtitle {
|
|
150
|
+
font-size: 14px;
|
|
151
|
+
color: #6b7280;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.item-pin {
|
|
160
|
+
width: fit-content;
|
|
161
|
+
position: relative;
|
|
162
|
+
display: flex;
|
|
163
|
+
gap: 16px;
|
|
164
|
+
padding: 2px 6px;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.item-compact {
|
|
170
|
+
width: 100%;
|
|
171
|
+
border: 1px solid #e5e7eb;
|
|
172
|
+
border-radius: 6px;
|
|
173
|
+
padding: 4px;
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: row;
|
|
176
|
+
gap: 12px;
|
|
177
|
+
justify-content: start;
|
|
178
|
+
align-items: center;
|
|
179
|
+
.image {
|
|
180
|
+
width: 60px;
|
|
181
|
+
height: 60px;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
}
|
|
184
|
+
.non-image {
|
|
185
|
+
max-width: 240px;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attributeInScope,
|
|
3
|
+
getAttributesByHasRole,
|
|
4
|
+
getAttributesWithoutRoles,
|
|
5
|
+
} from '../../helpers/attribute.helper';
|
|
6
|
+
import { isAttributeType } from '../../helpers/spec.helper';
|
|
7
|
+
import { ItemViewConfig } from '../../spec/spec';
|
|
8
|
+
import {
|
|
9
|
+
FetchedAttributeType,
|
|
10
|
+
FetchedItemType,
|
|
11
|
+
ViewOptions,
|
|
12
|
+
} from '../../spec/spec.internal';
|
|
13
|
+
import { Attribute } from '../../renderer/attribute';
|
|
14
|
+
import './item-view.scss';
|
|
15
|
+
export interface ItemCompactType extends ItemViewConfig {
|
|
16
|
+
type: 'compact';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ItemCompact = ({
|
|
20
|
+
options,
|
|
21
|
+
item,
|
|
22
|
+
index,
|
|
23
|
+
className,
|
|
24
|
+
style,
|
|
25
|
+
}: {
|
|
26
|
+
options: ViewOptions;
|
|
27
|
+
item: FetchedItemType | undefined;
|
|
28
|
+
index: number;
|
|
29
|
+
className?: string;
|
|
30
|
+
style?: React.CSSProperties;
|
|
31
|
+
}) => {
|
|
32
|
+
if (!item) return <></>;
|
|
33
|
+
|
|
34
|
+
const shown = options.overview.shownAttributes;
|
|
35
|
+
const labeledAttributes = item.attributes
|
|
36
|
+
.flatMap((a) => getAttributesByHasRole(a, true))
|
|
37
|
+
.filter((a) => attributeInScope(shown, a));
|
|
38
|
+
|
|
39
|
+
const unlabeledAttributes = getAttributesWithoutRoles(item) ?? [];
|
|
40
|
+
|
|
41
|
+
const sliceMax = Math.min(0, 4 - labeledAttributes.length);
|
|
42
|
+
|
|
43
|
+
const CompactAttribute = ({
|
|
44
|
+
attribute,
|
|
45
|
+
}: {
|
|
46
|
+
attribute: FetchedAttributeType;
|
|
47
|
+
}) => (
|
|
48
|
+
<Attribute
|
|
49
|
+
className={`sm
|
|
50
|
+
${
|
|
51
|
+
isAttributeType(attribute) && attribute.type === 'image'
|
|
52
|
+
? 'image'
|
|
53
|
+
: 'non-image'
|
|
54
|
+
}`}
|
|
55
|
+
options={options}
|
|
56
|
+
attribute={attribute}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
console.log(unlabeledAttributes);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={`item-compact ${className}`} style={style}>
|
|
64
|
+
{labeledAttributes.map((attribute) => (
|
|
65
|
+
<CompactAttribute key={attribute?.id} attribute={attribute} />
|
|
66
|
+
))}
|
|
67
|
+
{unlabeledAttributes.slice(0, sliceMax).map((attribute) => (
|
|
68
|
+
<CompactAttribute key={attribute?.id} attribute={attribute} />
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|