@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.
Files changed (61) hide show
  1. package/README.md +63 -0
  2. package/package.json +52 -0
  3. package/postcss.config.mjs +5 -0
  4. package/rollup.config.js +51 -0
  5. package/src/assets/add-tab.svg +4 -0
  6. package/src/assets/chevron-right.svg +4 -0
  7. package/src/assets/delete-tab.svg +3 -0
  8. package/src/assets/dummy-data/skeleton.json +42 -0
  9. package/src/assets/dummy-data/skeleton.ts +28 -0
  10. package/src/assets/meridian-toggle.svg +4 -0
  11. package/src/components/attributes/attribute-price.tsx +17 -0
  12. package/src/components/detail-views/detail-basic.tsx +121 -0
  13. package/src/components/detail-views/detail-view.scss +187 -0
  14. package/src/components/item-views/item-compact.tsx +72 -0
  15. package/src/components/item-views/item-pin.tsx +131 -0
  16. package/src/components/item-views/item-profile.tsx +140 -0
  17. package/src/components/item-views/item-vertical.tsx +145 -0
  18. package/src/components/item-views/item-view.scss +277 -0
  19. package/src/components/malleability/console/console-setting.tsx +184 -0
  20. package/src/components/malleability/console/console-view.tsx +47 -0
  21. package/src/components/malleability/console/detail-view-component.tsx +262 -0
  22. package/src/components/malleability/console/malleability-component.tsx +104 -0
  23. package/src/components/malleability/console/malleability-console.scss +285 -0
  24. package/src/components/malleability/console/overview-component.tsx +174 -0
  25. package/src/components/malleability/malleability-content-toggle.tsx +32 -0
  26. package/src/components/malleability/malleability-overview-tabs.tsx +212 -0
  27. package/src/components/malleability/malleability-toolbar.tsx +15 -0
  28. package/src/components/malleability/malleability.scss +199 -0
  29. package/src/components/overviews/overivew-basic-table.tsx +127 -0
  30. package/src/components/overviews/overview-basic-grid.tsx +27 -0
  31. package/src/components/overviews/overview-basic-list.tsx +61 -0
  32. package/src/components/overviews/overview-basic-map.tsx +358 -0
  33. package/src/components/overviews/overview-basic.scss +88 -0
  34. package/src/components/ui/dropdown-menu.tsx +61 -0
  35. package/src/helpers/attribute-set.helper.ts +4 -0
  36. package/src/helpers/attribute.helper.ts +334 -0
  37. package/src/helpers/spec.helper.ts +92 -0
  38. package/src/helpers/utils.helper.ts +22 -0
  39. package/src/helpers/view.helper.ts +184 -0
  40. package/src/index.css +149 -0
  41. package/src/index.ts +1 -0
  42. package/src/renderer/attribute.scss +59 -0
  43. package/src/renderer/attribute.tsx +305 -0
  44. package/src/renderer/renderer.data-bind.ts +573 -0
  45. package/src/renderer/renderer.defaults.ts +194 -0
  46. package/src/renderer/renderer.denormalize.ts +273 -0
  47. package/src/renderer/renderer.filter.ts +211 -0
  48. package/src/renderer/renderer.props.ts +21 -0
  49. package/src/renderer/renderer.scss +72 -0
  50. package/src/renderer/renderer.tsx +450 -0
  51. package/src/renderer/wrapper.tsx +225 -0
  52. package/src/spec/spec.internal.ts +76 -0
  53. package/src/spec/spec.ts +195 -0
  54. package/src/store/odi-malleability.store.ts +337 -0
  55. package/src/store/odi-navigation.store.ts +44 -0
  56. package/src/store/odi.store.ts +210 -0
  57. package/tailwind.config.js +31 -0
  58. package/tsconfig.json +24 -0
  59. package/types/svg.d.ts +6 -0
  60. package/vercel.json +5 -0
  61. 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
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ }
5
+ }
@@ -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,4 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M6 1L6 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
3
+ <path d="M1 6L11 6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M1 9L5 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
3
+ <path d="M1 1L5 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
4
+ </svg>
@@ -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
+ };