├── .DS_Store ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── actions.vue ├── components │ ├── CardItem.vue │ ├── card.vue │ └── header.vue ├── index.ts ├── layout.vue ├── options.vue ├── shims.d.ts ├── style.css └── types.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codihaus/directus-extension-grid-layout/9884b49c4fb7064d9a1ef6bc7e2564eeaa4a0096/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release Packages 2 | on: 3 | release: 4 | types: [ created ] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js ${{ matrix.node-version }} 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: 20 14 | registry-url: https://registry.npmjs.org/ 15 | - name: Install dependencies 16 | run: pnpm ci 17 | - name: Build App 18 | run: npm run build 19 | - name: Publish to NPM 20 | run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v1.0.8 5 | 6 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.7...v1.0.8) 7 | 8 | ## v1.0.7 9 | 10 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.6...v1.0.7) 11 | 12 | ## v1.0.6 13 | 14 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.5...v1.0.6) 15 | 16 | ## v1.0.5 17 | 18 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.3...v1.0.5) 19 | 20 | ### 🩹 Fixes 21 | 22 | - #12 change component v-collection-field-template, to fix layout options ([#12](https://github.com/codihaus/directus-extension-grid-layout/issues/12)) 23 | 24 | ### 🏡 Chore 25 | 26 | - **release:** V1.0.3 ([5ecdd78](https://github.com/codihaus/directus-extension-grid-layout/commit/5ecdd78)) 27 | - **release:** V1.0.4 ([13a441b](https://github.com/codihaus/directus-extension-grid-layout/commit/13a441b)) 28 | 29 | ### ❤️ Contributors 30 | 31 | - TranQuangMinh 32 | - Thanhduong 33 | 34 | ## v1.0.4 35 | 36 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.3...v1.0.4) 37 | 38 | ## v1.0.3 39 | 40 | [compare changes](https://github.com/codihaus/directus-extension-grid-layout/compare/v1.0.21...v1.0.3) 41 | 42 | ### 🩹 Fixes 43 | 44 | - If no photo source is selected, show icon default #8 ([#8](https://github.com/codihaus/directus-extension-grid-layout/issues/8)) 45 | - Issue #10 ([#10](https://github.com/codihaus/directus-extension-grid-layout/issues/10)) 46 | - Issue #7 ([#7](https://github.com/codihaus/directus-extension-grid-layout/issues/7)) 47 | - Issue #6 ([#6](https://github.com/codihaus/directus-extension-grid-layout/issues/6)) 48 | 49 | ### ❤️ Contributors 50 | 51 | - Thanhduong 52 | - Van Tu 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # directus-extension-grid-layout 2 | 3 | Directus Grid Layout Extension is a custom display type that enhances the way data is displayed in Directus. It allows users to present data in a grid layout that is both intuitive and visually appealing. 4 | 5 | Extension by #CodiHaus - https://codihaus.com 6 | 7 | image 8 | 9 | 10 | # Installation via npm 11 | 12 | Run the command above at your Directu's root project level 13 | 14 | - npm i directus-extension-grid-layout 15 | 16 | # Installation by compiling 17 | 18 | - Clone the repo 19 | - Go to the repo folder 20 | - npm run build 21 | - Copy the index.js file on dist/index.js folder to /directus/extension/directus-extension-grid-layout folder 22 | - Restart your Directus instance & enjoy 23 | 24 | # How to use 25 | 26 | On your right sidebar, you would be able to select the Layout to display, select "Grid" 27 | By selecting grid, it will show you the additional configurations to select data to show on each item. 28 | 29 | image 30 | 31 | # Licensing 32 | 33 | GNUv3 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-extension-grid-layout", 3 | "description": "", 4 | "icon": "extension", 5 | "version": "1.0.8", 6 | "main": "./dist/index.js", 7 | "author": { 8 | "email": "contact@codihaus.com", 9 | "name": "Codihaus" 10 | }, 11 | "keywords": [ 12 | "directus", 13 | "directus-extension", 14 | "directus-grid-layout", 15 | "directus-blog-layout", 16 | "directus-admin-layout" 17 | ], 18 | "directus:extension": { 19 | "type": "layout", 20 | "path": "./dist/index.js", 21 | "source": "src/index.ts", 22 | "host": "^9.23.1", 23 | "hidden": false 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "directus-extension build", 30 | "dev": "directus-extension build -w --no-minify", 31 | "link": "directus-extension link", 32 | "release": "pnpm run build && changelogen --release && npm publish && git push --follow-tags" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/codihaus/directus-extension-grid-layout/" 37 | }, 38 | "devDependencies": { 39 | "@directus/extensions-sdk": "9.23.1", 40 | "sass": "^1.59.3", 41 | "sass-loader": "^13.2.1", 42 | "typescript": "^4.9.4", 43 | "vue": "^3.4.38", 44 | "vue-i18n": "^9.2.2" 45 | }, 46 | "dependencies": { 47 | "@directus/composables": "^10.1.6", 48 | "@directus/constants": "^11.0.1", 49 | "@directus/types": "^11.0.2", 50 | "@directus/utils": "^11.0.2", 51 | "changelogen": "^0.5.5" 52 | } 53 | } -------------------------------------------------------------------------------- /src/actions.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/CardItem.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 171 | 172 | -------------------------------------------------------------------------------- /src/components/card.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 294 | 295 | 518 | -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 139 | 140 | 326 | 327 | 435 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from "vue-i18n"; 2 | import { 3 | useCollection, 4 | useItems, 5 | useSync, 6 | defineLayout, 7 | } from "@directus/extensions-sdk"; 8 | import { getFieldsFromTemplate } from "@directus/utils"; 9 | import { 10 | computed, 11 | ref, 12 | toRefs, 13 | Ref, 14 | unref, 15 | watch, 16 | } from "vue"; 17 | import CardsActions from "./actions.vue"; 18 | import CardsLayout from "./layout.vue"; 19 | import CardsOptions from "./options.vue"; 20 | import { 21 | LayoutOptions, 22 | LayoutQuery, 23 | } from "./types"; 24 | import "./style.css"; 25 | 26 | export default defineLayout< 27 | LayoutOptions, 28 | LayoutQuery 29 | >({ 30 | id: "item_blog", 31 | name: "Grid View", 32 | icon: "grid_4", 33 | component: CardsLayout, 34 | slots: { 35 | options: CardsOptions, 36 | sidebar: () => undefined, 37 | actions: CardsActions, 38 | }, 39 | setup(props, { emit }) { 40 | const selection = useSync( 41 | props, 42 | "selection", 43 | emit 44 | ); 45 | const layoutOptions = useSync( 46 | props, 47 | "layoutOptions", 48 | emit 49 | ); 50 | 51 | const layoutQuery = useSync( 52 | props, 53 | "layoutQuery", 54 | emit 55 | ); 56 | 57 | const { 58 | collection, 59 | filter, 60 | search, 61 | filterUser, 62 | } = toRefs(props); 63 | 64 | const { 65 | info, 66 | primaryKeyField, 67 | fields: fieldsInCollection, 68 | } = useCollection(collection); 69 | 70 | const { 71 | cardstyle, 72 | size, 73 | icon, 74 | imageSource, 75 | title, 76 | subtitle, 77 | content, 78 | imageFit, 79 | tag, 80 | idShow, 81 | } = useLayoutOptions(); 82 | 83 | const { 84 | sort, 85 | limit, 86 | page, 87 | fields, 88 | } = useLayoutQuery(); 89 | 90 | const fileFields = computed( 91 | () => { 92 | return fieldsInCollection.value; 93 | } 94 | ); 95 | 96 | const { 97 | items, 98 | loading, 99 | error, 100 | totalPages, 101 | itemCount, 102 | totalCount, 103 | getItems, 104 | getTotalCount, 105 | getItemCount, 106 | } = useItems(collection, { 107 | sort, 108 | limit, 109 | page, 110 | fields, 111 | filter, 112 | search, 113 | }); 114 | 115 | function formatCollectionItemsCount( 116 | totalItems: number, 117 | currentPage: number, 118 | perPage: number, 119 | isFiltered = false 120 | ) { 121 | const { t, n } = useI18n(); 122 | 123 | const opts = { 124 | start: n( 125 | (+currentPage - 1) * 126 | perPage + 127 | 1 128 | ), 129 | end: n( 130 | Math.min( 131 | currentPage * 132 | perPage, 133 | totalItems || 0 134 | ) 135 | ), 136 | count: n( 137 | totalItems || 0 138 | ), 139 | }; 140 | 141 | if (isFiltered) { 142 | if (totalItems === 1) { 143 | return t( 144 | "one_filtered_item" 145 | ); 146 | } 147 | 148 | return t( 149 | "start_end_of_count_filtered_items", 150 | opts 151 | ); 152 | } 153 | 154 | if (totalItems > perPage) { 155 | return t( 156 | "start_end_of_count_items", 157 | opts 158 | ); 159 | } 160 | 161 | return t("item_count", { 162 | count: totalItems, 163 | }); 164 | } 165 | 166 | const showingCount = computed( 167 | () => { 168 | const filtering = 169 | Boolean( 170 | (itemCount.value || 171 | 0) < 172 | (totalCount.value || 173 | 0) && 174 | filterUser.value 175 | ); 176 | return formatCollectionItemsCount( 177 | itemCount.value || 178 | 0, 179 | page.value, 180 | limit.value, 181 | filtering 182 | ); 183 | } 184 | ); 185 | 186 | function syncRefProperty< 187 | R, 188 | T extends keyof R 189 | >( 190 | ref: Ref, 191 | key: T, 192 | defaultValue: 193 | | R[T] 194 | | Ref 195 | ) { 196 | return computed({ 197 | get() { 198 | return ( 199 | ref.value?.[ 200 | key 201 | ] ?? 202 | unref( 203 | defaultValue 204 | ) 205 | ); 206 | }, 207 | set(value: R[T]) { 208 | ref.value = 209 | Object.assign( 210 | {}, 211 | ref.value, 212 | { 213 | [key]: value, 214 | } 215 | ) as R; 216 | }, 217 | }); 218 | } 219 | 220 | const width = ref(0); 221 | 222 | const isSingleRow = computed( 223 | () => { 224 | const cardsWidth = 225 | items.value.length * 226 | (size.value * 227 | 40) + 228 | (items.value 229 | .length - 230 | 1) * 231 | 24; 232 | return ( 233 | cardsWidth <= 234 | width.value 235 | ); 236 | } 237 | ); 238 | 239 | function clone( 240 | value: any 241 | ): any { 242 | if ( 243 | typeof value === 244 | "object" && 245 | value !== null 246 | ) { 247 | if ( 248 | Array.isArray(value) 249 | ) { 250 | return value.map( 251 | (item) => 252 | clone(item) 253 | ) as unknown as any; 254 | } else { 255 | const newObj = 256 | {} as any; 257 | for (let prop in value) { 258 | newObj[prop] = 259 | clone( 260 | value[ 261 | prop 262 | ] 263 | ); 264 | } 265 | return newObj; 266 | } 267 | } 268 | return value; 269 | } 270 | 271 | async function resetPresetAndRefresh(): Promise { 272 | await props?.resetPreset?.(); 273 | refresh(); 274 | } 275 | 276 | function refresh(): void { 277 | getItems(); 278 | getTotalCount(); 279 | getItemCount(); 280 | } 281 | 282 | function toPage( 283 | newPage: number 284 | ) { 285 | page.value = newPage; 286 | } 287 | 288 | function useLayoutOptions() { 289 | const cardstyle = 290 | createViewOption( 291 | "cardstyle", 292 | '1' 293 | ); 294 | const size = 295 | createViewOption( 296 | "size", 297 | 1 298 | ); 299 | const icon = 300 | createViewOption( 301 | "icon", 302 | "box" 303 | ); 304 | const title = 305 | createViewOption< 306 | string | null 307 | >("title", null); 308 | const subtitle = 309 | createViewOption< 310 | string | null 311 | >("subtitle", null); 312 | const content = 313 | createViewOption< 314 | string | null 315 | >("content", null); 316 | const imageSource = 317 | createViewOption< 318 | string | null 319 | >("imageSource", null); 320 | const imageFit = 321 | createViewOption( 322 | "imageFit", 323 | "cover" 324 | ); 325 | const tag = 326 | createViewOption( 327 | "tag", 328 | null 329 | ); 330 | const idShow = 331 | createViewOption( 332 | "idShow", 333 | true 334 | ); 335 | return { 336 | cardstyle, 337 | size, 338 | icon, 339 | imageSource, 340 | title, 341 | subtitle, 342 | content, 343 | imageFit, 344 | tag, 345 | idShow, 346 | }; 347 | function createViewOption< 348 | T 349 | >( 350 | key: keyof LayoutOptions, 351 | defaultValue: any 352 | ) { 353 | return computed({ 354 | get() { 355 | return layoutOptions 356 | .value?.[ 357 | key 358 | ] !== undefined 359 | ? layoutOptions 360 | .value?.[ 361 | key 362 | ] 363 | : defaultValue; 364 | }, 365 | set(newValue: T) { 366 | layoutOptions.value = 367 | { 368 | ...layoutOptions.value, 369 | [key]: newValue, 370 | }; 371 | }, 372 | }); 373 | } 374 | } 375 | 376 | function useLayoutQuery() { 377 | const page = 378 | syncRefProperty( 379 | layoutQuery, 380 | "page", 381 | 1 382 | ); 383 | const limit = 384 | syncRefProperty( 385 | layoutQuery, 386 | "limit", 387 | 25 388 | ); 389 | const defaultSort = 390 | computed(() => 391 | primaryKeyField.value 392 | ? [ 393 | primaryKeyField 394 | .value 395 | ?.field, 396 | ] 397 | : [] 398 | ); 399 | const sort = 400 | syncRefProperty( 401 | layoutQuery, 402 | "sort", 403 | defaultSort 404 | ); 405 | 406 | const fields = computed< 407 | string[] 408 | >(() => { 409 | if ( 410 | !primaryKeyField.value || 411 | !props.collection 412 | ) 413 | return []; 414 | const fields = [ 415 | primaryKeyField 416 | .value.field, 417 | ]; 418 | 419 | if (imageSource.value) { 420 | fields.push( 421 | `${imageSource.value}.modified_on` 422 | ); 423 | fields.push( 424 | `${imageSource.value}.type` 425 | ); 426 | fields.push( 427 | `${imageSource.value}.filename_disk` 428 | ); 429 | fields.push( 430 | `${imageSource.value}.storage` 431 | ); 432 | fields.push( 433 | `${imageSource.value}.id` 434 | ); 435 | } 436 | 437 | if ( 438 | props.collection === 439 | "directus_files" && 440 | imageSource.value === 441 | "$thumbnail" 442 | ) { 443 | fields.push( 444 | "modified_on" 445 | ); 446 | fields.push("type"); 447 | } 448 | 449 | const titleSubtitleFields: string[] = 450 | []; 451 | 452 | if (title.value) { 453 | titleSubtitleFields.push( 454 | ...getFieldsFromTemplate( 455 | title.value 456 | ) 457 | ); 458 | } 459 | 460 | if (subtitle.value) { 461 | titleSubtitleFields.push( 462 | ...getFieldsFromTemplate( 463 | subtitle.value 464 | ) 465 | ); 466 | } 467 | if (content.value) { 468 | titleSubtitleFields.push( 469 | ...getFieldsFromTemplate( 470 | content.value 471 | ) 472 | ); 473 | } 474 | if (tag.value) { 475 | titleSubtitleFields.push( 476 | ...getFieldsFromTemplate( 477 | tag.value 478 | ) 479 | ); 480 | } 481 | 482 | titleSubtitleFields.push( 483 | "status" 484 | ); 485 | titleSubtitleFields.push( 486 | "date_created" 487 | ); 488 | 489 | return [ 490 | ...fields, 491 | ...titleSubtitleFields, 492 | ]; 493 | }); 494 | 495 | return { 496 | sort, 497 | limit, 498 | page, 499 | fields, 500 | }; 501 | } 502 | 503 | function getLinkForItem( 504 | item: Record 505 | ): string | undefined { 506 | if (!primaryKeyField.value) 507 | return; 508 | return `/content/${ 509 | props.collection 510 | }/${encodeURIComponent( 511 | item[ 512 | primaryKeyField 513 | .value.field 514 | ] 515 | )}`; 516 | } 517 | 518 | function selectAll(): void { 519 | if (!primaryKeyField.value) 520 | return; 521 | const pk = 522 | primaryKeyField.value; 523 | selection.value = clone( 524 | items.value 525 | )?.map( 526 | (item: any) => 527 | item[pk.field] 528 | ); 529 | } 530 | 531 | return { 532 | fileFields, 533 | items, 534 | loading, 535 | error, 536 | totalPages, 537 | page, 538 | toPage, 539 | itemCount, 540 | totalCount, 541 | fieldsInCollection, 542 | limit, 543 | cardstyle, 544 | size, 545 | primaryKeyField, 546 | icon, 547 | imageSource, 548 | tag, 549 | idShow, 550 | title, 551 | subtitle, 552 | content, 553 | getLinkForItem, 554 | imageFit, 555 | sort, 556 | info, 557 | showingCount, 558 | isSingleRow, 559 | width, 560 | refresh, 561 | selectAll, 562 | resetPresetAndRefresh, 563 | filter, 564 | search, 565 | }; 566 | }, 567 | }); 568 | -------------------------------------------------------------------------------- /src/layout.vue: -------------------------------------------------------------------------------- 1 | 192 | 193 | 462 | 463 | 514 | -------------------------------------------------------------------------------- /src/options.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 246 | 247 | 258 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .card .render-template { 2 | all: unset; 3 | } 4 | 5 | .card 6 | .render-template 7 | .vertical-aligner { 8 | all: unset; 9 | } 10 | 11 | .card-img-img { 12 | animation: scale2 0.05s ease-in-out 13 | forwards; 14 | } 15 | 16 | .scale { 17 | animation: scale 0.15s ease-in-out 18 | forwards; 19 | } 20 | 21 | .card .datetime { 22 | overflow: hidden; 23 | line-height: 1.15; 24 | white-space: initial !important; 25 | text-overflow: ellipsis; 26 | display: -webkit-box; 27 | -webkit-line-clamp: 1; 28 | -webkit-box-orient: vertical; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .card .tags { 33 | overflow: hidden; 34 | line-height: 1.15; 35 | white-space: initial !important; 36 | text-overflow: ellipsis; 37 | display: -webkit-box; 38 | -webkit-line-clamp: 1; 39 | -webkit-box-orient: vertical; 40 | text-overflow: ellipsis; 41 | } 42 | 43 | @keyframes scale { 44 | 0% { 45 | transform: scale(1); 46 | } 47 | 48 | 100% { 49 | transform: scale(0.98); 50 | } 51 | } 52 | 53 | @keyframes scale2 { 54 | 0% { 55 | transform: scale(0.98); 56 | } 57 | 58 | 100% { 59 | transform: scale(1); 60 | } 61 | } 62 | 63 | .w-full { 64 | width: 100%; 65 | } 66 | 67 | .h-full { 68 | height: 100%; 69 | } 70 | 71 | .items-center { 72 | align-items: center; 73 | } 74 | 75 | .card .flex-1 { 76 | -webkit-box-flex: 1; 77 | -ms-flex: 1 1 0%; 78 | -webkit-flex: 1 1 0%; 79 | flex: 1 1 0%; 80 | } 81 | 82 | .card .d-flex { 83 | display: flex; 84 | } 85 | 86 | .mr-1 { 87 | margin-right: 4px; 88 | } 89 | 90 | .mb-2 { 91 | margin-bottom: 8px; 92 | } 93 | 94 | .mb-1 { 95 | margin-bottom: 4px; 96 | } 97 | 98 | .mr-2 { 99 | margin-right: 8px; 100 | } 101 | 102 | .gap-2 { 103 | gap: 8px; 104 | } 105 | 106 | .gap-6-6 { 107 | gap: 24px; 108 | } 109 | 110 | .gap-6 { 111 | gap: 24px; 112 | } 113 | 114 | @media (max-width: 768px) { 115 | .gap-6-6 { 116 | gap: 16px; 117 | } 118 | } 119 | 120 | .outline { 121 | position: absolute; 122 | inset: 0; 123 | border-radius: var(--border-radius); 124 | overflow: hidden; 125 | box-shadow: inset 0 0 0 10px 126 | var(--primary-50); 127 | transition: box-shadow 0.15s 128 | ease-in-out; 129 | } 130 | 131 | .object-cover { 132 | object-fit: cover; 133 | } 134 | 135 | .object-contain { 136 | object-fit: contain; 137 | } 138 | 139 | .max-h-12 { 140 | max-height: 48px; 141 | overflow: hidden; 142 | } 143 | 144 | .relative { 145 | position: relative; 146 | } 147 | 148 | .chip-id { 149 | background-color: var( 150 | --border-normal 151 | ); 152 | color: var(--text-normal); 153 | } 154 | 155 | .card:hover .title { 156 | color: var(--primary); 157 | } 158 | 159 | .outline { 160 | position: absolute; 161 | inset: 0; 162 | border-radius: var(--border-radius); 163 | overflow: hidden; 164 | box-shadow: inset 0 0 0 10px 165 | var(--primary-50); 166 | transition: box-shadow 0.15s 167 | ease-in-out; 168 | } 169 | 170 | .color_primary { 171 | background-color: var(--primary); 172 | } 173 | 174 | .color_sub { 175 | background-color: var( 176 | --border-normal 177 | ); 178 | } 179 | 180 | .card-1 .d-flex-custom { 181 | display: flex; 182 | } 183 | 184 | .card-1 .card-img { 185 | width: 290px; 186 | height: 160px; 187 | object-fit: contain; 188 | border-radius: 6px; 189 | overflow: hidden; 190 | } 191 | 192 | .card-1 .card-img-img { 193 | width: 100%; 194 | height: 100%; 195 | background-color: var( 196 | --border-normal 197 | ); 198 | } 199 | 200 | @media (max-width: 768px) { 201 | .card-1 .card-img { 202 | width: 150px; 203 | height: 110px; 204 | } 205 | 206 | .card .card-1 .title { 207 | font-size: 1rem; 208 | line-height: 1.3; 209 | } 210 | 211 | .card .card-1 .subtitle { 212 | font-size: 1rem; 213 | line-height: 1.2; 214 | } 215 | 216 | .card .card-1 .tags { 217 | display: none; 218 | } 219 | } 220 | 221 | .card-2 .card-img { 222 | width: 180px; 223 | height: 160px; 224 | border-radius: var(--border-radius); 225 | overflow: hidden; 226 | } 227 | 228 | .card-2 .card-img-img { 229 | width: 100%; 230 | height: 100%; 231 | background-color: var( 232 | --border-normal 233 | ); 234 | } 235 | 236 | .card-2 .d-flex-custom { 237 | display: flex; 238 | } 239 | 240 | @media (max-width: 1024px) { 241 | .card-2 .d-flex-custom { 242 | flex-direction: column; 243 | } 244 | 245 | .card-2 .card-img { 246 | width: 100%; 247 | height: 180px; 248 | } 249 | 250 | .card .card-2 .title { 251 | font-size: 1.1rem; 252 | } 253 | 254 | .card .card-2 .subtitle { 255 | font-size: 1rem; 256 | } 257 | } 258 | 259 | .card-3 .card-img-img { 260 | width: 100%; 261 | height: 100%; 262 | background-color: var( 263 | --border-normal 264 | ); 265 | } 266 | 267 | .card-3 .d-flex-custom { 268 | display: flex; 269 | flex-direction: column !important; 270 | } 271 | 272 | .card-3 .card-img { 273 | width: 100%; 274 | height: 190px; 275 | object-fit: cover; 276 | border-radius: var(--border-radius); 277 | overflow: hidden; 278 | } 279 | .card .render-template span.label { 280 | vertical-align: bottom !important; 281 | } 282 | .card .render-template .toggle { 283 | margin: 0 10px; 284 | } 285 | 286 | 287 | 288 | .post-content table { 289 | width: 100% !important; 290 | } 291 | 292 | .post-content * { 293 | word-wrap: break-word; 294 | } 295 | 296 | .post-content h4 { 297 | font-size: 1.4rem; 298 | margin-bottom: 1rem; 299 | } 300 | 301 | .post-content h5 { 302 | font-size: 1.2rem; 303 | margin-bottom: 1rem; 304 | } 305 | 306 | .post-content h6 { 307 | font-size: 1rem; 308 | letter-spacing: 0.03125em; 309 | text-transform: uppercase; 310 | margin-bottom: 1rem; 311 | } 312 | 313 | .post-content * img { 314 | border-radius: 2px; 315 | max-width: 100%; 316 | margin: 16px 0; 317 | } 318 | 319 | .post-content p { 320 | margin-bottom: 1.2rem; 321 | text-align: justify; 322 | } 323 | 324 | .post-content h2 { 325 | font-size: 24px; 326 | font-weight: 600; 327 | margin-bottom: 24px; 328 | } 329 | 330 | .post-content p:first-child { 331 | margin-top: 0; 332 | } 333 | 334 | .post-content h3:nth-child(1) { 335 | margin-top: 0; 336 | } 337 | 338 | .post-content h3 { 339 | font-size: 20px; 340 | font-weight: 600; 341 | margin-bottom: 24px; 342 | } 343 | 344 | .post-content h4 { 345 | font-size: 18px; 346 | font-weight: 600; 347 | margin-bottom: 24px; 348 | } 349 | 350 | .post-content h5 { 351 | font-size: 16px; 352 | font-weight: 600; 353 | margin-bottom: 24px; 354 | } 355 | 356 | .post-content ul { 357 | margin-bottom: 24px; 358 | } 359 | 360 | .post-content ul li + li { 361 | margin-top: 12px; 362 | } 363 | 364 | .post-content li { 365 | position: relative; 366 | padding-left: 16px; 367 | } 368 | 369 | .post-content li::after { 370 | content: ''; 371 | width: 6px; 372 | height: 6px; 373 | border-radius: 100%; 374 | position: absolute; 375 | top: 10px; 376 | left: 0; 377 | } 378 | 379 | .post-content td { 380 | padding-left: 8px; 381 | } 382 | 383 | .post-content iframe { 384 | width: 100%; 385 | height: 100%; 386 | aspect-ratio: 16/9; 387 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type LayoutOptions = { 2 | size?: number; 3 | icon?: string; 4 | imageSource?: string; 5 | title?: string; 6 | subtitle?: string; 7 | imageFit?: 'crop' | 'contain';, 8 | tag?: string; 9 | idShow?: boolean; 10 | }; 11 | 12 | export type LayoutQuery = { 13 | fields: string[]; 14 | sort: string[]; 15 | limit: number; 16 | page: number; 17 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "lib": ["ES2019", "DOM"], 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noUnusedParameters": true, 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "resolveJsonModule": false, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "allowSyntheticDefaultImports": true, 24 | "isolatedModules": true, 25 | "rootDir": "./src" 26 | }, 27 | "include": ["./src/**/*.ts"] 28 | } 29 | --------------------------------------------------------------------------------