mechanica 2.0.0-alpha.8 → 2.0.0-alpha.9

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/dist/cli.js CHANGED
@@ -302,7 +302,8 @@ async function backfillImageMeta(pages, blocksMap, mechDir, warn) {
302
302
  * `<cwd>/.mech`.
303
303
  *
304
304
  * Beyond the pages it also: rewrites + copies uploaded assets
305
- * (`/@mechanica/assets/…` → `/media/…`), warns about dead internal links,
305
+ * (`/@mechanica/assets/…` → `/media/…`, or `<assetsUrl>/media/…` when a CDN
306
+ * base is set), warns about dead internal links,
306
307
  * orphaned uploads and page-level SEO issues, emits `404.html` when a `/404`
307
308
  * page exists, and — when a site url is known (plugin `siteUrl` option or
308
309
  * `--site-url`) — injects the automatic SEO tags (canonical, `og:url`,
@@ -314,6 +315,7 @@ async function exportProject(cwd, ssr, options = {}) {
314
315
  const index = await readFile(join(cwd, "dist/index.html"), "utf-8");
315
316
  const siteUrl = options.siteUrl ?? ssr.site?.url;
316
317
  const siteName = options.siteName ?? ssr.site?.name;
318
+ const assetsBase = (options.assetsUrl ?? ssr.assetsUrl ?? "").replace(/\/+$/, "");
317
319
  const composedDefs = ssr.composedList ?? [];
318
320
  const blocks = [...ssr.blocksList.map(toBlockMeta), ...composedDefs.map(composedBlockMeta)];
319
321
  const blocksMap = new Map(blocks.map((block) => [block.id, block]));
@@ -420,7 +422,7 @@ async function exportProject(cwd, ssr, options = {}) {
420
422
  if (typeof src !== "string" || !src.startsWith("/@mechanica/assets/")) return src;
421
423
  const relative = decodeURIComponent(src.slice(UPLOADS_PREFIX.length));
422
424
  referenced.add(relative);
423
- return `/${MEDIA_DIR}/${relative}`;
425
+ return `${assetsBase}/${MEDIA_DIR}/${relative}`;
424
426
  };
425
427
  const blockAssets = await readBlockAssets(cwd);
426
428
  const pageLinks = blockAssets ? (content) => {
@@ -447,6 +449,7 @@ async function exportProject(cwd, ssr, options = {}) {
447
449
  url: siteUrl,
448
450
  name: siteName
449
451
  },
452
+ assetsUrl: assetsBase || void 0,
450
453
  locales: config ?? void 0,
451
454
  render: (state) => ssr.render(state, { resolveQuery: resolveForPage(state.page?.pagination?.page ?? 1, state.page?.locale) }),
452
455
  onFile,
@@ -1364,7 +1367,9 @@ Usage:
1364
1367
  (a site url — the plugin's siteUrl option or
1365
1368
  --site-url <origin> — also turns on the automatic
1366
1369
  SEO tags and emits sitemap.xml + robots.txt;
1367
- --site-name <name> adds WebSite JSON-LD)
1370
+ --site-name <name> adds WebSite JSON-LD;
1371
+ --assets-url <origin> serves /assets and /media from
1372
+ a CDN base — files still land in export/)
1368
1373
  mechanica push [--key] Upload dist/ to a backend (--key, --host, --dir)
1369
1374
  mechanica shot <blockId> Screenshot one block via the dev preview route
1370
1375
  mechanica shot </path> Screenshot a whole page (editor overlay stripped)
@@ -1389,7 +1394,8 @@ async function run(argv) {
1389
1394
  case "build": return runBuild();
1390
1395
  case "export": return runExport({
1391
1396
  siteUrl: str(flags["site-url"]),
1392
- siteName: str(flags["site-name"])
1397
+ siteName: str(flags["site-name"]),
1398
+ assetsUrl: str(flags["assets-url"])
1393
1399
  });
1394
1400
  case "images": return runImages({ force: flags.force === true });
1395
1401
  case "push": return runPush({
package/dist/editor.css CHANGED
@@ -4802,7 +4802,7 @@ from {
4802
4802
  justify-content: flex-end;
4803
4803
  gap: 8px;
4804
4804
  margin-top: 4px;
4805
- }.mech-pages[data-v-5f70933a] {
4805
+ }.mech-pages[data-v-665bb42e] {
4806
4806
  display: flex;
4807
4807
  align-items: stretch;
4808
4808
  justify-content: center;
@@ -4811,7 +4811,7 @@ from {
4811
4811
  max-width: 1200px;
4812
4812
  height: min(620px, 82vh);
4813
4813
  }
4814
- .mech-pages__card[data-v-5f70933a] {
4814
+ .mech-pages__card[data-v-665bb42e] {
4815
4815
  display: flex;
4816
4816
  flex-direction: column;
4817
4817
  flex: 0 1 840px;
@@ -4821,20 +4821,20 @@ from {
4821
4821
  box-shadow: var(--mech-shadow-dialog);
4822
4822
  overflow: hidden;
4823
4823
  }
4824
- .mech-pages__header[data-v-5f70933a] {
4824
+ .mech-pages__header[data-v-665bb42e] {
4825
4825
  display: flex;
4826
4826
  align-items: center;
4827
4827
  gap: 10px;
4828
4828
  padding: 18px 14px 12px 24px;
4829
4829
  flex: none;
4830
4830
  }
4831
- .mech-pages__title[data-v-5f70933a] {
4831
+ .mech-pages__title[data-v-665bb42e] {
4832
4832
  margin: 0;
4833
4833
  font-size: 16px;
4834
4834
  font-weight: 600;
4835
4835
  letter-spacing: -0.01em;
4836
4836
  }
4837
- .mech-pages__count[data-v-5f70933a] {
4837
+ .mech-pages__count[data-v-665bb42e] {
4838
4838
  padding: 2px 8px;
4839
4839
  border-radius: var(--mech-radius-pill);
4840
4840
  background: var(--mech-active);
@@ -4842,7 +4842,7 @@ from {
4842
4842
  font-size: 11.5px;
4843
4843
  font-weight: 600;
4844
4844
  }
4845
- .mech-pages__close[data-v-5f70933a] {
4845
+ .mech-pages__close[data-v-665bb42e] {
4846
4846
  display: flex;
4847
4847
  align-items: center;
4848
4848
  justify-content: center;
@@ -4855,25 +4855,49 @@ from {
4855
4855
  border-radius: var(--mech-radius-pill);
4856
4856
  cursor: pointer;
4857
4857
  }
4858
- .mech-pages__close .vicon[data-v-5f70933a] {
4858
+ .mech-pages__close .vicon[data-v-665bb42e] {
4859
4859
  width: 18px;
4860
4860
  height: 18px;
4861
4861
  }
4862
- .mech-pages__close[data-v-5f70933a]:hover {
4862
+ .mech-pages__close[data-v-665bb42e]:hover {
4863
4863
  background: var(--mech-hover);
4864
4864
  color: var(--mech-fg);
4865
4865
  }
4866
- .mech-pages__toolbar[data-v-5f70933a] {
4866
+ .mech-pages__toolbar[data-v-665bb42e] {
4867
4867
  display: flex;
4868
4868
  gap: 8px;
4869
- padding: 0 24px 14px;
4869
+ padding: 0 24px 12px;
4870
4870
  flex: none;
4871
4871
  }
4872
- .mech-pages__search-wrap[data-v-5f70933a] {
4872
+ .mech-pages__search-wrap[data-v-665bb42e] {
4873
4873
  position: relative;
4874
4874
  flex: 1;
4875
4875
  }
4876
- .mech-pages__search-icon[data-v-5f70933a] {
4876
+ .mech-pages__filters[data-v-665bb42e] {
4877
+ display: flex;
4878
+ align-items: center;
4879
+ justify-content: space-between;
4880
+ gap: 12px;
4881
+ padding: 0 24px 14px;
4882
+ flex: none;
4883
+ }
4884
+ .mech-pages__sort[data-v-665bb42e] {
4885
+ display: flex;
4886
+ align-items: center;
4887
+ gap: 9px;
4888
+ }
4889
+ .mech-pages__sort-label[data-v-665bb42e] {
4890
+ font-size: 10.5px;
4891
+ font-weight: 600;
4892
+ text-transform: uppercase;
4893
+ letter-spacing: 0.05em;
4894
+ color: var(--mech-muted);
4895
+ }
4896
+ .mech-pages__toggle[data-v-665bb42e] {
4897
+ flex: none;
4898
+ white-space: nowrap;
4899
+ }
4900
+ .mech-pages__search-icon[data-v-665bb42e] {
4877
4901
  position: absolute;
4878
4902
  left: 12px;
4879
4903
  top: 50%;
@@ -4883,18 +4907,18 @@ from {
4883
4907
  color: var(--mech-muted);
4884
4908
  pointer-events: none;
4885
4909
  }
4886
- .mech-pages__search[data-v-5f70933a] {
4910
+ .mech-pages__search[data-v-665bb42e] {
4887
4911
  width: 100%;
4888
4912
  padding-left: 36px;
4889
4913
  }
4890
- .mech-pages__content[data-v-5f70933a] {
4914
+ .mech-pages__content[data-v-665bb42e] {
4891
4915
  display: flex;
4892
4916
  gap: 14px;
4893
4917
  flex: 1;
4894
4918
  min-height: 0;
4895
4919
  padding: 0 24px 20px;
4896
4920
  }
4897
- .mech-pages__rail[data-v-5f70933a] {
4921
+ .mech-pages__rail[data-v-665bb42e] {
4898
4922
  flex: none;
4899
4923
  width: 172px;
4900
4924
  display: flex;
@@ -4903,7 +4927,7 @@ from {
4903
4927
  overflow-y: auto;
4904
4928
  padding-right: 2px;
4905
4929
  }
4906
- .mech-pages__rail-item[data-v-5f70933a] {
4930
+ .mech-pages__rail-item[data-v-665bb42e] {
4907
4931
  display: flex;
4908
4932
  align-items: center;
4909
4933
  gap: 8px;
@@ -4917,51 +4941,51 @@ from {
4917
4941
  color: var(--mech-fg-alt);
4918
4942
  cursor: pointer;
4919
4943
  }
4920
- .mech-pages__rail-item .vicon[data-v-5f70933a] {
4944
+ .mech-pages__rail-item .vicon[data-v-665bb42e] {
4921
4945
  flex: none;
4922
4946
  width: 15px;
4923
4947
  height: 15px;
4924
4948
  color: var(--mech-muted);
4925
4949
  }
4926
- .mech-pages__rail-item[data-v-5f70933a]:hover {
4950
+ .mech-pages__rail-item[data-v-665bb42e]:hover {
4927
4951
  background: var(--mech-hover);
4928
4952
  }
4929
- .mech-pages__rail-item.is-active[data-v-5f70933a] {
4953
+ .mech-pages__rail-item.is-active[data-v-665bb42e] {
4930
4954
  background: var(--mech-accent-soft);
4931
4955
  color: var(--mech-accent);
4932
4956
  font-weight: 500;
4933
4957
  }
4934
- .mech-pages__rail-item.is-active .vicon[data-v-5f70933a] {
4958
+ .mech-pages__rail-item.is-active .vicon[data-v-665bb42e] {
4935
4959
  color: var(--mech-accent);
4936
4960
  }
4937
- .mech-pages__rail-name[data-v-5f70933a] {
4961
+ .mech-pages__rail-name[data-v-665bb42e] {
4938
4962
  flex: 1;
4939
4963
  min-width: 0;
4940
4964
  overflow: hidden;
4941
4965
  text-overflow: ellipsis;
4942
4966
  white-space: nowrap;
4943
4967
  }
4944
- .mech-pages__rail-count[data-v-5f70933a] {
4968
+ .mech-pages__rail-count[data-v-665bb42e] {
4945
4969
  flex: none;
4946
4970
  font-size: 11px;
4947
4971
  color: var(--mech-placeholder);
4948
4972
  }
4949
- .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-5f70933a] {
4973
+ .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-665bb42e] {
4950
4974
  color: var(--mech-accent);
4951
4975
  opacity: 0.7;
4952
4976
  }
4953
- .mech-pages__list[data-v-5f70933a] {
4977
+ .mech-pages__list[data-v-665bb42e] {
4954
4978
  flex: 1;
4955
4979
  min-width: 0;
4956
4980
  overflow-y: auto;
4957
4981
  }
4958
- .mech-pages__row[data-v-5f70933a], .mech-pages__thead[data-v-5f70933a] {
4982
+ .mech-pages__row[data-v-665bb42e], .mech-pages__thead[data-v-665bb42e] {
4959
4983
  display: grid;
4960
4984
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) 96px;
4961
4985
  align-items: center;
4962
4986
  column-gap: 12px;
4963
4987
  }
4964
- .mech-pages__thead[data-v-5f70933a] {
4988
+ .mech-pages__thead[data-v-665bb42e] {
4965
4989
  position: sticky;
4966
4990
  top: 0;
4967
4991
  z-index: 1;
@@ -4969,14 +4993,14 @@ from {
4969
4993
  background: var(--mech-bg);
4970
4994
  box-shadow: 0 1px 0 var(--mech-border);
4971
4995
  }
4972
- .mech-pages__thead span[data-v-5f70933a] {
4996
+ .mech-pages__thead span[data-v-665bb42e] {
4973
4997
  font-size: 10.5px;
4974
4998
  font-weight: 600;
4975
4999
  text-transform: uppercase;
4976
5000
  letter-spacing: 0.05em;
4977
5001
  color: var(--mech-muted);
4978
5002
  }
4979
- .mech-pages__group[data-v-5f70933a] {
5003
+ .mech-pages__group[data-v-665bb42e] {
4980
5004
  display: flex;
4981
5005
  align-items: center;
4982
5006
  gap: 7px;
@@ -4995,36 +5019,36 @@ from {
4995
5019
  border-radius: var(--mech-radius-sm);
4996
5020
  cursor: pointer;
4997
5021
  }
4998
- .mech-pages__group .vicon[data-v-5f70933a] {
5022
+ .mech-pages__group .vicon[data-v-665bb42e] {
4999
5023
  width: 13px;
5000
5024
  height: 13px;
5001
5025
  }
5002
- .mech-pages__group[data-v-5f70933a]:hover {
5026
+ .mech-pages__group[data-v-665bb42e]:hover {
5003
5027
  color: var(--mech-fg);
5004
5028
  }
5005
- .mech-pages__row[data-v-5f70933a] {
5029
+ .mech-pages__row[data-v-665bb42e] {
5006
5030
  padding: 0 10px;
5007
5031
  height: 38px;
5008
5032
  border-radius: var(--mech-radius-sm);
5009
5033
  cursor: pointer;
5010
5034
  }
5011
- .mech-pages__row.is-active[data-v-5f70933a] {
5035
+ .mech-pages__row.is-active[data-v-665bb42e] {
5012
5036
  background: var(--mech-accent-soft);
5013
5037
  }
5014
- .mech-pages__cell-name[data-v-5f70933a] {
5038
+ .mech-pages__cell-name[data-v-665bb42e] {
5015
5039
  display: flex;
5016
5040
  align-items: center;
5017
5041
  gap: 8px;
5018
5042
  min-width: 0;
5019
5043
  }
5020
- .mech-pages__name[data-v-5f70933a] {
5044
+ .mech-pages__name[data-v-665bb42e] {
5021
5045
  font-size: 13.5px;
5022
5046
  font-weight: 500;
5023
5047
  overflow: hidden;
5024
5048
  text-overflow: ellipsis;
5025
5049
  white-space: nowrap;
5026
5050
  }
5027
- .mech-pages__badge[data-v-5f70933a] {
5051
+ .mech-pages__badge[data-v-665bb42e] {
5028
5052
  flex: none;
5029
5053
  font-size: 10px;
5030
5054
  font-weight: 600;
@@ -5036,17 +5060,17 @@ from {
5036
5060
  border-radius: var(--mech-radius-pill);
5037
5061
  padding: 1px 7px;
5038
5062
  }
5039
- .mech-pages__badge.is-draft[data-v-5f70933a] {
5063
+ .mech-pages__badge.is-draft[data-v-665bb42e] {
5040
5064
  color: var(--mech-muted);
5041
5065
  }
5042
- .mech-pages__locales[data-v-5f70933a] {
5066
+ .mech-pages__locales[data-v-665bb42e] {
5043
5067
  flex: none;
5044
5068
  display: inline-flex;
5045
5069
  align-items: center;
5046
5070
  gap: 3px;
5047
5071
  margin-left: 2px;
5048
5072
  }
5049
- .mech-pages__loc[data-v-5f70933a] {
5073
+ .mech-pages__loc[data-v-665bb42e] {
5050
5074
  min-width: 15px;
5051
5075
  padding: 1px 4px;
5052
5076
  border-radius: var(--mech-radius-sm);
@@ -5060,12 +5084,12 @@ from {
5060
5084
  letter-spacing: 0.02em;
5061
5085
  opacity: 0.5;
5062
5086
  }
5063
- .mech-pages__loc.is-on[data-v-5f70933a] {
5087
+ .mech-pages__loc.is-on[data-v-665bb42e] {
5064
5088
  background: var(--mech-accent-soft);
5065
5089
  color: var(--mech-accent);
5066
5090
  opacity: 1;
5067
5091
  }
5068
- .mech-pages__cell-path[data-v-5f70933a] {
5092
+ .mech-pages__cell-path[data-v-665bb42e] {
5069
5093
  min-width: 0;
5070
5094
  overflow: hidden;
5071
5095
  text-overflow: ellipsis;
@@ -5073,16 +5097,16 @@ from {
5073
5097
  font-size: 12.5px;
5074
5098
  color: var(--mech-muted);
5075
5099
  }
5076
- .mech-pages__actions[data-v-5f70933a] {
5100
+ .mech-pages__actions[data-v-665bb42e] {
5077
5101
  display: flex;
5078
5102
  justify-content: flex-end;
5079
5103
  gap: 1px;
5080
5104
  opacity: 0;
5081
5105
  }
5082
- .mech-pages__row.is-active .mech-pages__actions[data-v-5f70933a], .mech-pages__row:hover .mech-pages__actions[data-v-5f70933a] {
5106
+ .mech-pages__row.is-active .mech-pages__actions[data-v-665bb42e], .mech-pages__row:hover .mech-pages__actions[data-v-665bb42e] {
5083
5107
  opacity: 1;
5084
5108
  }
5085
- .mech-pages__actions button[data-v-5f70933a] {
5109
+ .mech-pages__actions button[data-v-665bb42e] {
5086
5110
  display: flex;
5087
5111
  align-items: center;
5088
5112
  justify-content: center;
@@ -5095,24 +5119,24 @@ from {
5095
5119
  cursor: pointer;
5096
5120
  line-height: 1;
5097
5121
  }
5098
- .mech-pages__actions button .vicon[data-v-5f70933a] {
5122
+ .mech-pages__actions button .vicon[data-v-665bb42e] {
5099
5123
  width: 15px;
5100
5124
  height: 15px;
5101
5125
  }
5102
- .mech-pages__actions button[data-v-5f70933a]:hover {
5126
+ .mech-pages__actions button[data-v-665bb42e]:hover {
5103
5127
  background: var(--mech-active);
5104
5128
  color: var(--mech-fg);
5105
5129
  }
5106
- .mech-pages__actions button.is-danger[data-v-5f70933a]:hover {
5130
+ .mech-pages__actions button.is-danger[data-v-665bb42e]:hover {
5107
5131
  color: var(--mech-error);
5108
5132
  }
5109
- .mech-pages__empty[data-v-5f70933a] {
5133
+ .mech-pages__empty[data-v-665bb42e] {
5110
5134
  padding: 24px 10px;
5111
5135
  margin: 0;
5112
5136
  color: var(--mech-muted);
5113
5137
  font-size: 13px;
5114
5138
  }
5115
- .mech-pages__preview[data-v-5f70933a] {
5139
+ .mech-pages__preview[data-v-665bb42e] {
5116
5140
  flex: none;
5117
5141
  align-self: flex-start;
5118
5142
  max-height: 100%;
@@ -5124,18 +5148,18 @@ from {
5124
5148
  box-shadow: var(--mech-shadow-dialog);
5125
5149
  overflow: hidden;
5126
5150
  }
5127
- .mech-pages__preview-frame[data-v-5f70933a] {
5151
+ .mech-pages__preview-frame[data-v-665bb42e] {
5128
5152
  flex: 0 1 auto;
5129
5153
  min-height: 0;
5130
5154
  overflow: hidden;
5131
5155
  background: var(--mech-app-bg);
5132
5156
  }
5133
- .mech-pages__preview-frame img[data-v-5f70933a] {
5157
+ .mech-pages__preview-frame img[data-v-665bb42e] {
5134
5158
  display: block;
5135
5159
  width: 100%;
5136
5160
  height: auto;
5137
5161
  }
5138
- .mech-pages__preview-empty[data-v-5f70933a] {
5162
+ .mech-pages__preview-empty[data-v-665bb42e] {
5139
5163
  display: flex;
5140
5164
  flex-direction: column;
5141
5165
  align-items: center;
@@ -5144,16 +5168,16 @@ from {
5144
5168
  padding: 48px 16px;
5145
5169
  color: var(--mech-muted);
5146
5170
  }
5147
- .mech-pages__preview-empty p[data-v-5f70933a] {
5171
+ .mech-pages__preview-empty p[data-v-665bb42e] {
5148
5172
  margin: 8px 0 0;
5149
5173
  font-size: 13px;
5150
5174
  font-weight: 500;
5151
5175
  }
5152
- .mech-pages__preview-empty code[data-v-5f70933a] {
5176
+ .mech-pages__preview-empty code[data-v-665bb42e] {
5153
5177
  font-size: 11.5px;
5154
5178
  color: var(--mech-placeholder);
5155
5179
  }
5156
- .mech-pages__preview-monogram[data-v-5f70933a] {
5180
+ .mech-pages__preview-monogram[data-v-665bb42e] {
5157
5181
  display: flex;
5158
5182
  align-items: center;
5159
5183
  justify-content: center;
@@ -5164,7 +5188,7 @@ from {
5164
5188
  font-size: 22px;
5165
5189
  font-weight: 600;
5166
5190
  }
5167
- .mech-pages__preview-caption[data-v-5f70933a] {
5191
+ .mech-pages__preview-caption[data-v-665bb42e] {
5168
5192
  flex: none;
5169
5193
  display: flex;
5170
5194
  flex-direction: column;
@@ -5172,14 +5196,14 @@ from {
5172
5196
  padding: 12px 16px 14px;
5173
5197
  border-top: 1px solid var(--mech-border);
5174
5198
  }
5175
- .mech-pages__preview-name[data-v-5f70933a] {
5199
+ .mech-pages__preview-name[data-v-665bb42e] {
5176
5200
  font-size: 13px;
5177
5201
  font-weight: 600;
5178
5202
  overflow: hidden;
5179
5203
  text-overflow: ellipsis;
5180
5204
  white-space: nowrap;
5181
5205
  }
5182
- .mech-pages__preview-path[data-v-5f70933a] {
5206
+ .mech-pages__preview-path[data-v-665bb42e] {
5183
5207
  font-size: 11.5px;
5184
5208
  color: var(--mech-muted);
5185
5209
  overflow: hidden;
@@ -5187,7 +5211,7 @@ from {
5187
5211
  white-space: nowrap;
5188
5212
  }
5189
5213
  @media (max-width: 1160px) {
5190
- .mech-pages__preview[data-v-5f70933a] {
5214
+ .mech-pages__preview[data-v-665bb42e] {
5191
5215
  display: none;
5192
5216
  }
5193
5217
  }.mech-pagebar[data-v-a0f85ad0] {
package/dist/editor.js CHANGED
@@ -3,7 +3,7 @@ import { f as createRouter, h as mechanicaKey, i as renderBlocks, n as pushState
3
3
  import { i as getDataEntries, t as buildPreviewContent } from "./chunks/preview-G5_AFI5P.js";
4
4
  import { A as groupPagesByFolder, C as registerBuiltinFieldEditors, D as VSegmented_default, E as createContextMenu, M as pathFromName, O as fetchPages, P as VPopover_default, T as contextMenuKey, _ as findParentSlot, b as relocateBlock, c as compareBlocks, f as cloneBlock, h as findBlock, j as pageThumbUrl, k as filterPages, l as createContentBlock, n as createSaveQueue, o as shortcutChar, p as duplicateBlock, r as SchemaForm_default, s as blockAvailableIn, t as SaveConflictError, u as toBlockMeta, v as moveBlock, w as registerRichTextWidgets, x as removeBlock, y as placeBlock } from "./chunks/editor-9tquvDcS.js";
5
5
  import { c as useDialog, d as _plugin_vue_export_helper_default, l as VIcon_default, n as VDialog_default, o as createDialogStore, s as dialogKey } from "./chunks/ImagePickerDialog-CY-mn2UP.js";
6
- import { Fragment, Teleport, Transition, TransitionGroup, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, markRaw, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, onScopeDispose, openBlock, provide, reactive, ref, renderList, resolveComponent, resolveDynamicComponent, shallowRef, toDisplayString, unref, useTemplateRef, vModelText, watch, withCtx, withDirectives, withModifiers } from "vue";
6
+ import { Fragment, Teleport, Transition, TransitionGroup, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, markRaw, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, onScopeDispose, openBlock, provide, reactive, ref, renderList, resolveComponent, resolveDynamicComponent, shallowRef, toDisplayString, unref, useTemplateRef, vModelCheckbox, vModelText, watch, withCtx, withDirectives, withModifiers } from "vue";
7
7
  import { getDefaultValue, localeLabel, localePath, parseLocalePath, registerFieldSchemas } from "mechanica-shared";
8
8
  import { blocksList } from "virtual:mechanica/blocks";
9
9
  import { composedList } from "virtual:mechanica/composed";
@@ -1886,62 +1886,70 @@ var _hoisted_3$3 = { class: "mech-pages__header" };
1886
1886
  var _hoisted_4$3 = { class: "mech-pages__count" };
1887
1887
  var _hoisted_5$2 = { class: "mech-pages__toolbar" };
1888
1888
  var _hoisted_6$2 = { class: "mech-pages__search-wrap" };
1889
- var _hoisted_7$2 = { class: "mech-pages__content" };
1890
- var _hoisted_8$2 = {
1889
+ var _hoisted_7$2 = { class: "mech-pages__filters" };
1890
+ var _hoisted_8$2 = { class: "mech-pages__sort" };
1891
+ var _hoisted_9$2 = {
1892
+ key: 0,
1893
+ class: "mech-checkbox mech-pages__toggle"
1894
+ };
1895
+ var _hoisted_10$2 = { class: "mech-pages__content" };
1896
+ var _hoisted_11$1 = {
1891
1897
  key: 0,
1892
1898
  class: "mech-pages__rail"
1893
1899
  };
1894
- var _hoisted_9$2 = { class: "mech-pages__rail-count" };
1895
- var _hoisted_10$2 = ["onClick"];
1896
- var _hoisted_11$1 = { class: "mech-pages__rail-name" };
1897
1900
  var _hoisted_12 = { class: "mech-pages__rail-count" };
1898
1901
  var _hoisted_13 = ["onClick"];
1899
- var _hoisted_14 = [
1902
+ var _hoisted_14 = { class: "mech-pages__rail-name" };
1903
+ var _hoisted_15 = { class: "mech-pages__rail-count" };
1904
+ var _hoisted_16 = ["onClick"];
1905
+ var _hoisted_17 = [
1900
1906
  "onClick",
1901
1907
  "onMouseenter",
1902
1908
  "onContextmenu"
1903
1909
  ];
1904
- var _hoisted_15 = { class: "mech-pages__cell-name" };
1905
- var _hoisted_16 = { class: "mech-pages__name" };
1906
- var _hoisted_17 = {
1910
+ var _hoisted_18 = { class: "mech-pages__cell-name" };
1911
+ var _hoisted_19 = { class: "mech-pages__name" };
1912
+ var _hoisted_20 = {
1907
1913
  key: 0,
1908
1914
  class: "mech-pages__badge is-draft"
1909
1915
  };
1910
- var _hoisted_18 = {
1916
+ var _hoisted_21 = {
1911
1917
  key: 1,
1912
1918
  class: "mech-pages__badge"
1913
1919
  };
1914
- var _hoisted_19 = {
1920
+ var _hoisted_22 = {
1915
1921
  key: 2,
1916
1922
  class: "mech-pages__badge"
1917
1923
  };
1918
- var _hoisted_20 = {
1924
+ var _hoisted_23 = {
1919
1925
  key: 3,
1920
1926
  class: "mech-pages__locales"
1921
1927
  };
1922
- var _hoisted_21 = ["title"];
1923
- var _hoisted_22 = { class: "mech-pages__cell-path" };
1924
- var _hoisted_23 = ["onClick"];
1925
- var _hoisted_24 = ["onClick"];
1926
- var _hoisted_25 = ["onClick"];
1927
- var _hoisted_26 = {
1928
+ var _hoisted_24 = ["title"];
1929
+ var _hoisted_25 = { class: "mech-pages__cell-path" };
1930
+ var _hoisted_26 = ["onClick"];
1931
+ var _hoisted_27 = ["onClick"];
1932
+ var _hoisted_28 = ["onClick"];
1933
+ var _hoisted_29 = {
1928
1934
  key: 0,
1929
1935
  class: "mech-pages__empty"
1930
1936
  };
1931
- var _hoisted_27 = { class: "mech-pages__preview" };
1932
- var _hoisted_28 = { class: "mech-pages__preview-frame" };
1933
- var _hoisted_29 = ["src", "data-path"];
1934
- var _hoisted_30 = {
1937
+ var _hoisted_30 = { class: "mech-pages__preview" };
1938
+ var _hoisted_31 = { class: "mech-pages__preview-frame" };
1939
+ var _hoisted_32 = ["src", "data-path"];
1940
+ var _hoisted_33 = {
1935
1941
  key: 1,
1936
1942
  class: "mech-pages__preview-empty"
1937
1943
  };
1938
- var _hoisted_31 = { class: "mech-pages__preview-monogram" };
1939
- var _hoisted_32 = {
1944
+ var _hoisted_34 = { class: "mech-pages__preview-monogram" };
1945
+ var _hoisted_35 = {
1940
1946
  key: 0,
1941
1947
  class: "mech-pages__preview-caption"
1942
1948
  };
1943
- var _hoisted_33 = { class: "mech-pages__preview-name" };
1944
- var _hoisted_34 = { class: "mech-pages__preview-path" };
1949
+ var _hoisted_36 = { class: "mech-pages__preview-name" };
1950
+ var _hoisted_37 = { class: "mech-pages__preview-path" };
1951
+ var SHOW_GENERATED_KEY = "mech:pages:show-generated";
1952
+ var SORT_KEY = "mech:pages:sort";
1945
1953
  //#endregion
1946
1954
  //#region src/editor/dialogs/PagesDialog.vue
1947
1955
  var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
@@ -1963,20 +1971,42 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
1963
1971
  const searchInput = useTemplateRef("searchInput");
1964
1972
  const listEl = useTemplateRef("listEl");
1965
1973
  const searching = computed(() => !!query.value.trim());
1974
+ const showGenerated = ref(typeof localStorage === "undefined" || localStorage.getItem(SHOW_GENERATED_KEY) !== "0");
1975
+ watch(showGenerated, (value) => {
1976
+ try {
1977
+ localStorage.setItem(SHOW_GENERATED_KEY, value ? "1" : "0");
1978
+ } catch {}
1979
+ });
1980
+ const hasGenerated = computed(() => pages.value.some((page) => page.generated));
1981
+ const listedPages = computed(() => showGenerated.value ? pages.value : pages.value.filter((page) => !page.generated));
1982
+ const sortKey = ref((typeof localStorage !== "undefined" && localStorage.getItem(SORT_KEY)) === "name" ? "name" : "path");
1983
+ watch(sortKey, (value) => {
1984
+ try {
1985
+ localStorage.setItem(SORT_KEY, value);
1986
+ } catch {}
1987
+ });
1988
+ const sortOptions = [{
1989
+ value: "path",
1990
+ label: "Path"
1991
+ }, {
1992
+ value: "name",
1993
+ label: "Name"
1994
+ }];
1966
1995
  const folders = computed(() => {
1967
1996
  const counts = /* @__PURE__ */ new Map();
1968
- for (const page of pages.value) if (page.folderPath) counts.set(page.folderPath, (counts.get(page.folderPath) ?? 0) + 1);
1997
+ for (const page of listedPages.value) if (page.folderPath) counts.set(page.folderPath, (counts.get(page.folderPath) ?? 0) + 1);
1969
1998
  return [...counts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({
1970
1999
  name,
1971
2000
  count
1972
2001
  }));
1973
2002
  });
1974
2003
  const visible = computed(() => {
1975
- if (searching.value) return filterPages(pages.value, query.value);
1976
- if (activeFolder.value) return pages.value.filter((page) => page.folderPath === activeFolder.value);
1977
- return pages.value;
2004
+ if (searching.value) return filterPages(listedPages.value, query.value);
2005
+ if (activeFolder.value) return listedPages.value.filter((page) => page.folderPath === activeFolder.value);
2006
+ return listedPages.value;
1978
2007
  });
1979
- const groups = computed(() => groupPagesByFolder(visible.value));
2008
+ const sorted = computed(() => [...visible.value].sort((a, b) => sortKey.value === "name" ? (a.name || a.path).localeCompare(b.name || b.path) : a.path.localeCompare(b.path)));
2009
+ const groups = computed(() => groupPagesByFolder(sorted.value));
1980
2010
  /** Flat list in render order — keyboard navigation walks this. */
1981
2011
  const rows = computed(() => groups.value.flatMap((group) => group.pages));
1982
2012
  const showGroupHeaders = computed(() => searching.value || !activeFolder.value);
@@ -1985,7 +2015,7 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
1985
2015
  onMounted(async () => {
1986
2016
  await load();
1987
2017
  searchInput.value?.focus();
1988
- activePath.value = pages.value.some((page) => page.path === current.value) ? current.value : rows.value[0]?.path ?? null;
2018
+ activePath.value = rows.value.some((page) => page.path === current.value) ? current.value : rows.value[0]?.path ?? null;
1989
2019
  });
1990
2020
  watch(rows, (list) => {
1991
2021
  if (!list.some((page) => page.path === activePath.value)) activePath.value = list[0]?.path ?? null;
@@ -2135,8 +2165,8 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2135
2165
  return (_ctx, _cache) => {
2136
2166
  return openBlock(), createElementBlock("div", _hoisted_1$4, [createElementVNode("div", _hoisted_2$4, [
2137
2167
  createElementVNode("header", _hoisted_3$3, [
2138
- _cache[4] || (_cache[4] = createElementVNode("h2", { class: "mech-pages__title" }, "Pages", -1)),
2139
- createElementVNode("span", _hoisted_4$3, toDisplayString(pages.value.length), 1),
2168
+ _cache[6] || (_cache[6] = createElementVNode("h2", { class: "mech-pages__title" }, "Pages", -1)),
2169
+ createElementVNode("span", _hoisted_4$3, toDisplayString(listedPages.value.length), 1),
2140
2170
  createElementVNode("button", {
2141
2171
  type: "button",
2142
2172
  class: "mech-pages__close",
@@ -2159,15 +2189,25 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2159
2189
  type: "button",
2160
2190
  class: "mech-button is-primary",
2161
2191
  onClick: startCreate
2162
- }, [createVNode(VIcon_default, { name: "plus" }), _cache[5] || (_cache[5] = createTextVNode(" New page ", -1))])]),
2163
- createElementVNode("div", _hoisted_7$2, [folders.value.length ? (openBlock(), createElementBlock("nav", _hoisted_8$2, [createElementVNode("button", {
2192
+ }, [createVNode(VIcon_default, { name: "plus" }), _cache[7] || (_cache[7] = createTextVNode(" New page ", -1))])]),
2193
+ createElementVNode("div", _hoisted_7$2, [createElementVNode("div", _hoisted_8$2, [_cache[8] || (_cache[8] = createElementVNode("span", { class: "mech-pages__sort-label" }, "Sort", -1)), createVNode(VSegmented_default, {
2194
+ modelValue: sortKey.value,
2195
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => sortKey.value = $event),
2196
+ options: sortOptions,
2197
+ size: "sm",
2198
+ "aria-label": "Sort pages"
2199
+ }, null, 8, ["modelValue"])]), hasGenerated.value ? (openBlock(), createElementBlock("label", _hoisted_9$2, [withDirectives(createElementVNode("input", {
2200
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => showGenerated.value = $event),
2201
+ type: "checkbox"
2202
+ }, null, 512), [[vModelCheckbox, showGenerated.value]]), _cache[9] || (_cache[9] = createElementVNode("span", null, "Show generated pages", -1))])) : createCommentVNode("", true)]),
2203
+ createElementVNode("div", _hoisted_10$2, [folders.value.length ? (openBlock(), createElementBlock("nav", _hoisted_11$1, [createElementVNode("button", {
2164
2204
  type: "button",
2165
2205
  class: normalizeClass(["mech-pages__rail-item", { "is-active": !activeFolder.value && !searching.value }]),
2166
- onClick: _cache[2] || (_cache[2] = ($event) => selectFolder(null))
2206
+ onClick: _cache[4] || (_cache[4] = ($event) => selectFolder(null))
2167
2207
  }, [
2168
2208
  createVNode(VIcon_default, { name: "book" }),
2169
- _cache[6] || (_cache[6] = createElementVNode("span", { class: "mech-pages__rail-name" }, "All pages", -1)),
2170
- createElementVNode("span", _hoisted_9$2, toDisplayString(pages.value.length), 1)
2209
+ _cache[10] || (_cache[10] = createElementVNode("span", { class: "mech-pages__rail-name" }, "All pages", -1)),
2210
+ createElementVNode("span", _hoisted_12, toDisplayString(listedPages.value.length), 1)
2171
2211
  ], 2), (openBlock(true), createElementBlock(Fragment, null, renderList(folders.value, (folder) => {
2172
2212
  return openBlock(), createElementBlock("button", {
2173
2213
  key: folder.name,
@@ -2176,15 +2216,15 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2176
2216
  onClick: ($event) => selectFolder(folder.name)
2177
2217
  }, [
2178
2218
  createVNode(VIcon_default, { name: "folder" }),
2179
- createElementVNode("span", _hoisted_11$1, toDisplayString(folder.name), 1),
2180
- createElementVNode("span", _hoisted_12, toDisplayString(folder.count), 1)
2181
- ], 10, _hoisted_10$2);
2219
+ createElementVNode("span", _hoisted_14, toDisplayString(folder.name), 1),
2220
+ createElementVNode("span", _hoisted_15, toDisplayString(folder.count), 1)
2221
+ ], 10, _hoisted_13);
2182
2222
  }), 128))])) : createCommentVNode("", true), createElementVNode("div", {
2183
2223
  ref_key: "listEl",
2184
2224
  ref: listEl,
2185
2225
  class: "mech-pages__list"
2186
2226
  }, [
2187
- _cache[7] || (_cache[7] = createElementVNode("div", { class: "mech-pages__thead" }, [
2227
+ _cache[11] || (_cache[11] = createElementVNode("div", { class: "mech-pages__thead" }, [
2188
2228
  createElementVNode("span", null, "Name"),
2189
2229
  createElementVNode("span", null, "Path"),
2190
2230
  createElementVNode("span", { "aria-hidden": "true" })
@@ -2196,7 +2236,7 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2196
2236
  class: "mech-pages__group",
2197
2237
  title: "Show only this folder",
2198
2238
  onClick: ($event) => selectFolder(group.folder)
2199
- }, [createVNode(VIcon_default, { name: "folder" }), createTextVNode(" " + toDisplayString(group.folder), 1)], 8, _hoisted_13)) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(group.pages, (page) => {
2239
+ }, [createVNode(VIcon_default, { name: "folder" }), createTextVNode(" " + toDisplayString(group.folder), 1)], 8, _hoisted_16)) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(group.pages, (page) => {
2200
2240
  return openBlock(), createElementBlock("div", {
2201
2241
  key: page.path,
2202
2242
  class: normalizeClass(["mech-pages__row", {
@@ -2207,57 +2247,57 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2207
2247
  onMouseenter: ($event) => activePath.value = page.path,
2208
2248
  onContextmenu: ($event) => onRowMenu($event, page)
2209
2249
  }, [
2210
- createElementVNode("span", _hoisted_15, [
2211
- createElementVNode("span", _hoisted_16, toDisplayString(page.name || page.path), 1),
2212
- page.draft ? (openBlock(), createElementBlock("span", _hoisted_17, "Draft")) : createCommentVNode("", true),
2213
- page.generated ? (openBlock(), createElementBlock("span", _hoisted_18, "Generated")) : createCommentVNode("", true),
2214
- page.path === currentLogical.value ? (openBlock(), createElementBlock("span", _hoisted_19, "Current")) : createCommentVNode("", true),
2215
- unref(localeConfig) && page.locales ? (openBlock(), createElementBlock("span", _hoisted_20, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(localeCodes), (code) => {
2250
+ createElementVNode("span", _hoisted_18, [
2251
+ createElementVNode("span", _hoisted_19, toDisplayString(page.name || page.path), 1),
2252
+ page.draft ? (openBlock(), createElementBlock("span", _hoisted_20, "Draft")) : createCommentVNode("", true),
2253
+ page.generated ? (openBlock(), createElementBlock("span", _hoisted_21, "Generated")) : createCommentVNode("", true),
2254
+ page.path === currentLogical.value ? (openBlock(), createElementBlock("span", _hoisted_22, "Current")) : createCommentVNode("", true),
2255
+ unref(localeConfig) && page.locales ? (openBlock(), createElementBlock("span", _hoisted_23, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(localeCodes), (code) => {
2216
2256
  return openBlock(), createElementBlock("span", {
2217
2257
  key: code,
2218
2258
  class: normalizeClass(["mech-pages__loc", { "is-on": page.locales.includes(code) }]),
2219
2259
  title: `${localeName(code)}: ${page.locales.includes(code) ? "translated" : "not translated"}`
2220
- }, toDisplayString(code), 11, _hoisted_21);
2260
+ }, toDisplayString(code), 11, _hoisted_24);
2221
2261
  }), 128))])) : createCommentVNode("", true)
2222
2262
  ]),
2223
- createElementVNode("span", _hoisted_22, toDisplayString(page.path), 1),
2263
+ createElementVNode("span", _hoisted_25, toDisplayString(page.path), 1),
2224
2264
  !page.generated ? (openBlock(), createElementBlock("span", {
2225
2265
  key: 0,
2226
2266
  class: "mech-pages__actions",
2227
- onClick: _cache[3] || (_cache[3] = withModifiers(() => {}, ["stop"]))
2267
+ onClick: _cache[5] || (_cache[5] = withModifiers(() => {}, ["stop"]))
2228
2268
  }, [
2229
2269
  createElementVNode("button", {
2230
2270
  type: "button",
2231
2271
  title: "Rename",
2232
2272
  onClick: ($event) => startEdit(page)
2233
- }, [createVNode(VIcon_default, { name: "pencil" })], 8, _hoisted_23),
2273
+ }, [createVNode(VIcon_default, { name: "pencil" })], 8, _hoisted_26),
2234
2274
  createElementVNode("button", {
2235
2275
  type: "button",
2236
2276
  title: "Duplicate",
2237
2277
  onClick: ($event) => startDuplicate(page)
2238
- }, [createVNode(VIcon_default, { name: "copy" })], 8, _hoisted_24),
2278
+ }, [createVNode(VIcon_default, { name: "copy" })], 8, _hoisted_27),
2239
2279
  createElementVNode("button", {
2240
2280
  type: "button",
2241
2281
  title: "Delete",
2242
2282
  class: "is-danger",
2243
2283
  onClick: ($event) => confirmRemove(page)
2244
- }, [createVNode(VIcon_default, { name: "trash" })], 8, _hoisted_25)
2284
+ }, [createVNode(VIcon_default, { name: "trash" })], 8, _hoisted_28)
2245
2285
  ])) : createCommentVNode("", true)
2246
- ], 42, _hoisted_14);
2286
+ ], 42, _hoisted_17);
2247
2287
  }), 128))], 64);
2248
2288
  }), 128)),
2249
- !rows.value.length ? (openBlock(), createElementBlock("p", _hoisted_26, toDisplayString(query.value ? `No pages match “${query.value}”.` : "No pages yet."), 1)) : createCommentVNode("", true)
2289
+ !rows.value.length ? (openBlock(), createElementBlock("p", _hoisted_29, toDisplayString(query.value ? `No pages match “${query.value}”.` : "No pages yet."), 1)) : createCommentVNode("", true)
2250
2290
  ], 512)])
2251
- ]), createElementVNode("aside", _hoisted_27, [createElementVNode("div", _hoisted_28, [previewPage.value && !thumbFailed.has(previewPage.value.path) ? (openBlock(), createElementBlock("img", {
2291
+ ]), createElementVNode("aside", _hoisted_30, [createElementVNode("div", _hoisted_31, [previewPage.value && !thumbFailed.has(previewPage.value.path) ? (openBlock(), createElementBlock("img", {
2252
2292
  key: previewPage.value.path,
2253
2293
  src: thumbSrc(previewPage.value),
2254
2294
  "data-path": previewPage.value.path,
2255
2295
  alt: "",
2256
2296
  onError: onThumbError
2257
- }, null, 40, _hoisted_29)) : (openBlock(), createElementBlock("div", _hoisted_30, [createElementVNode("span", _hoisted_31, toDisplayString(monogram.value), 1), previewPage.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[8] || (_cache[8] = createElementVNode("p", null, "No preview yet", -1)), _cache[9] || (_cache[9] = createElementVNode("code", null, "mechanica thumbs", -1))], 64)) : createCommentVNode("", true)]))]), previewPage.value ? (openBlock(), createElementBlock("div", _hoisted_32, [createElementVNode("span", _hoisted_33, toDisplayString(previewPage.value.name || previewPage.value.path), 1), createElementVNode("span", _hoisted_34, toDisplayString(previewPage.value.path), 1)])) : createCommentVNode("", true)])]);
2297
+ }, null, 40, _hoisted_32)) : (openBlock(), createElementBlock("div", _hoisted_33, [createElementVNode("span", _hoisted_34, toDisplayString(monogram.value), 1), previewPage.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[12] || (_cache[12] = createElementVNode("p", null, "No preview yet", -1)), _cache[13] || (_cache[13] = createElementVNode("code", null, "mechanica thumbs", -1))], 64)) : createCommentVNode("", true)]))]), previewPage.value ? (openBlock(), createElementBlock("div", _hoisted_35, [createElementVNode("span", _hoisted_36, toDisplayString(previewPage.value.name || previewPage.value.path), 1), createElementVNode("span", _hoisted_37, toDisplayString(previewPage.value.path), 1)])) : createCommentVNode("", true)])]);
2258
2298
  };
2259
2299
  }
2260
- }), [["__scopeId", "data-v-5f70933a"]]);
2300
+ }), [["__scopeId", "data-v-665bb42e"]]);
2261
2301
  //#endregion
2262
2302
  //#region src/editor/components/PageBar.vue?vue&type=script&setup=true&lang.ts
2263
2303
  var _hoisted_1$3 = { class: "mech-pagebar__icon" };
package/dist/plugin.js CHANGED
@@ -775,6 +775,7 @@ function generateSsrEntry(options) {
775
775
  ``,
776
776
  `registerComponents(blocksMap)`,
777
777
  `export const site = ${JSON.stringify(options.site ?? {})}`,
778
+ `export const assetsUrl = ${JSON.stringify(options.assetsUrl ?? null)}`,
778
779
  `export const locales = ${JSON.stringify(options.locales ?? null)}`,
779
780
  `export const generatedPages = ${JSON.stringify(options.generatedPages ?? [])}`,
780
781
  `export const dataEntries = getDataEntries()`,
@@ -1689,6 +1690,7 @@ function mechanica(options = {}) {
1689
1690
  url: options.siteUrl,
1690
1691
  name: options.siteName
1691
1692
  },
1693
+ assetsUrl: options.assetsUrl,
1692
1694
  locales,
1693
1695
  generatedPages: await loadGeneratedPages()
1694
1696
  });
@@ -22,6 +22,8 @@ export interface SsrBundle {
22
22
  url?: string;
23
23
  name?: string;
24
24
  };
25
+ /** CDN base URL for static assets, baked from the plugin's `assetsUrl` option. */
26
+ assetsUrl?: string | null;
25
27
  /** The site's locale config baked in from the plugin's `locales` option. */
26
28
  locales?: LocalesConfig | null;
27
29
  /** Programmatically generated pages (plugin `generatePages`), baked as plain
@@ -37,6 +39,12 @@ export interface ExportOptions {
37
39
  siteUrl?: string;
38
40
  /** Site display name (WebSite JSON-LD). Overrides the plugin's `siteName`. */
39
41
  siteName?: string;
42
+ /**
43
+ * Base URL (CDN origin) to serve static assets from — prefixes both the build
44
+ * assets (`/assets/…`) and uploaded media (`/media/…`). Overrides the plugin's
45
+ * `assetsUrl`. Files still land in `export/`; upload them to the CDN yourself.
46
+ */
47
+ assetsUrl?: string;
40
48
  /** Warning sink (broken links, orphaned assets). Defaults to `console.warn`. */
41
49
  onWarn?: (message: string) => void;
42
50
  }
@@ -49,7 +57,8 @@ export interface ExportOptions {
49
57
  * `<cwd>/.mech`.
50
58
  *
51
59
  * Beyond the pages it also: rewrites + copies uploaded assets
52
- * (`/@mechanica/assets/…` → `/media/…`), warns about dead internal links,
60
+ * (`/@mechanica/assets/…` → `/media/…`, or `<assetsUrl>/media/…` when a CDN
61
+ * base is set), warns about dead internal links,
53
62
  * orphaned uploads and page-level SEO issues, emits `404.html` when a `/404`
54
63
  * page exists, and — when a site url is known (plugin `siteUrl` option or
55
64
  * `--site-url`) — injects the automatic SEO tags (canonical, `og:url`,
@@ -58,6 +58,9 @@ export interface SsrEntryOptions {
58
58
  url?: string;
59
59
  name?: string;
60
60
  };
61
+ /** CDN base URL for static assets (plugin `assetsUrl`) — rides the bundle so
62
+ * `mechanica export` can serve `/assets/…` and `/media/…` from it. */
63
+ assetsUrl?: string;
61
64
  /** The site's locale config (multi-language) — rides the bundle so the export
62
65
  * can walk translations, and gets baked into each page's `state.locales`. */
63
66
  locales?: import('mechanica-shared').LocalesConfig | null;
@@ -50,6 +50,21 @@ export interface MechanicaPluginOptions {
50
50
  siteUrl?: string;
51
51
  /** The site's display name — `{{ site.name }}` + WebSite JSON-LD at export. */
52
52
  siteName?: string;
53
+ /**
54
+ * Base URL to serve static assets from at export — a CDN origin like
55
+ * `https://cdn.example.com`. When set, `mechanica export` rewrites both the
56
+ * build assets (`/assets/…`) and uploaded media (`/media/…`) to
57
+ * `<assetsUrl>/assets/…` and `<assetsUrl>/media/…`. Files are still emitted
58
+ * into `export/` — upload them to the CDN yourself (e.g. in CI). A trailing
59
+ * slash is ignored. `mechanica export --assets-url <origin>` overrides it.
60
+ *
61
+ * Only affects the static export; the dev server keeps serving assets
62
+ * locally. For build assets referenced from inside CSS/JS chunks (fonts,
63
+ * `url(...)` backgrounds) on a cross-origin CDN, set Vite's `base` instead —
64
+ * this option rewrites HTML asset tags and uploaded media, not chunk-internal
65
+ * references.
66
+ */
67
+ assetsUrl?: string;
53
68
  /**
54
69
  * Multi-language configuration. Either a full config
55
70
  * (`{ default: 'en', all: ['en', 'ru'], labels? }`) or a bare list of codes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mechanica",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "Build Vue 3 websites with a visual block editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "prepublishOnly": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "mechanica-shared": "2.0.0-alpha.8",
66
+ "mechanica-shared": "2.0.0-alpha.9",
67
67
  "@vue/compiler-sfc": "^3.5.38",
68
68
  "compact-json-schema": "^0.1.5",
69
69
  "estree-walker": "^3.0.3",
package/src/cli/cli.ts CHANGED
@@ -14,7 +14,9 @@ Usage:
14
14
  (a site url — the plugin's siteUrl option or
15
15
  --site-url <origin> — also turns on the automatic
16
16
  SEO tags and emits sitemap.xml + robots.txt;
17
- --site-name <name> adds WebSite JSON-LD)
17
+ --site-name <name> adds WebSite JSON-LD;
18
+ --assets-url <origin> serves /assets and /media from
19
+ a CDN base — files still land in export/)
18
20
  mechanica push [--key] Upload dist/ to a backend (--key, --host, --dir)
19
21
  mechanica shot <blockId> Screenshot one block via the dev preview route
20
22
  mechanica shot </path> Screenshot a whole page (editor overlay stripped)
@@ -42,7 +44,11 @@ export async function run(argv: string[]): Promise<void> {
42
44
  case 'build':
43
45
  return runBuild()
44
46
  case 'export':
45
- return runExport({ siteUrl: str(flags['site-url']), siteName: str(flags['site-name']) })
47
+ return runExport({
48
+ siteUrl: str(flags['site-url']),
49
+ siteName: str(flags['site-name']),
50
+ assetsUrl: str(flags['assets-url']),
51
+ })
46
52
  case 'images':
47
53
  return runImages({ force: flags.force === true })
48
54
  case 'push':
package/src/cli/export.ts CHANGED
@@ -227,6 +227,8 @@ export interface SsrBundle {
227
227
  dataEntries?: { id: string; props: any }[]
228
228
  /** Site identity baked in from the plugin's `siteUrl` / `siteName` options. */
229
229
  site?: { url?: string; name?: string }
230
+ /** CDN base URL for static assets, baked from the plugin's `assetsUrl` option. */
231
+ assetsUrl?: string | null
230
232
  /** The site's locale config baked in from the plugin's `locales` option. */
231
233
  locales?: LocalesConfig | null
232
234
  /** Programmatically generated pages (plugin `generatePages`), baked as plain
@@ -268,6 +270,12 @@ export interface ExportOptions {
268
270
  siteUrl?: string
269
271
  /** Site display name (WebSite JSON-LD). Overrides the plugin's `siteName`. */
270
272
  siteName?: string
273
+ /**
274
+ * Base URL (CDN origin) to serve static assets from — prefixes both the build
275
+ * assets (`/assets/…`) and uploaded media (`/media/…`). Overrides the plugin's
276
+ * `assetsUrl`. Files still land in `export/`; upload them to the CDN yourself.
277
+ */
278
+ assetsUrl?: string
271
279
  /** Warning sink (broken links, orphaned assets). Defaults to `console.warn`. */
272
280
  onWarn?: (message: string) => void
273
281
  }
@@ -375,7 +383,8 @@ async function backfillImageMeta(
375
383
  * `<cwd>/.mech`.
376
384
  *
377
385
  * Beyond the pages it also: rewrites + copies uploaded assets
378
- * (`/@mechanica/assets/…` → `/media/…`), warns about dead internal links,
386
+ * (`/@mechanica/assets/…` → `/media/…`, or `<assetsUrl>/media/…` when a CDN
387
+ * base is set), warns about dead internal links,
379
388
  * orphaned uploads and page-level SEO issues, emits `404.html` when a `/404`
380
389
  * page exists, and — when a site url is known (plugin `siteUrl` option or
381
390
  * `--site-url`) — injects the automatic SEO tags (canonical, `og:url`,
@@ -393,6 +402,9 @@ export async function exportProject(
393
402
  // CLI flags win over the site config baked into the SSR bundle.
394
403
  const siteUrl = options.siteUrl ?? ssr.site?.url
395
404
  const siteName = options.siteName ?? ssr.site?.name
405
+ // CDN base for static assets (plugin `assetsUrl` or `--assets-url`); trim a
406
+ // trailing slash so we build `<base>/media/…` and `<base>/assets/…` cleanly.
407
+ const assetsBase = (options.assetsUrl ?? ssr.assetsUrl ?? '').replace(/\/+$/, '')
396
408
 
397
409
  // Composed blocks join the block set: their (unfolded) schemas fill placed
398
410
  // data defaults, and they count as known blocks for link/unknown checks.
@@ -536,7 +548,8 @@ export async function exportProject(
536
548
  if (typeof src !== 'string' || !src.startsWith(UPLOADS_PREFIX)) return src
537
549
  const relative = decodeURIComponent(src.slice(UPLOADS_PREFIX.length))
538
550
  referenced.add(relative)
539
- return `/${MEDIA_DIR}/${relative}`
551
+ // `assetsBase` is a trimmed CDN origin (or '' — then this stays root-relative).
552
+ return `${assetsBase}/${MEDIA_DIR}/${relative}`
540
553
  }
541
554
 
542
555
  // Blocks are code-split in the client build: give each page stylesheet +
@@ -569,6 +582,9 @@ export async function exportProject(
569
582
  dataEntries: ssr.dataEntries ?? [],
570
583
  projectData,
571
584
  site: { url: siteUrl, name: siteName },
585
+ // Serve the build assets (`/assets/…` in the HTML) from the CDN base too, so
586
+ // the whole `export/` tree can be hosted there. Empty = keep them local.
587
+ assetsUrl: assetsBase || undefined,
572
588
  // Bake the locale config into each page's `state.locales` so the runtime
573
589
  // prefixes internal links for `state.page.locale`.
574
590
  locales: config ?? undefined,
@@ -3,7 +3,7 @@
3
3
  <div class="mech-pages__card" role="dialog" aria-modal="true">
4
4
  <header class="mech-pages__header">
5
5
  <h2 class="mech-pages__title">Pages</h2>
6
- <span class="mech-pages__count">{{ pages.length }}</span>
6
+ <span class="mech-pages__count">{{ listedPages.length }}</span>
7
7
  <button type="button" class="mech-pages__close" aria-label="Close" @click="dialog.back()">
8
8
  <VIcon name="close" />
9
9
  </button>
@@ -26,6 +26,18 @@
26
26
  </button>
27
27
  </div>
28
28
 
29
+ <!-- Filter / sort row — a home for sorting and toggles like generated pages. -->
30
+ <div class="mech-pages__filters">
31
+ <div class="mech-pages__sort">
32
+ <span class="mech-pages__sort-label">Sort</span>
33
+ <VSegmented v-model="sortKey" :options="sortOptions" size="sm" aria-label="Sort pages" />
34
+ </div>
35
+ <label v-if="hasGenerated" class="mech-checkbox mech-pages__toggle">
36
+ <input v-model="showGenerated" type="checkbox" />
37
+ <span>Show generated pages</span>
38
+ </label>
39
+ </div>
40
+
29
41
  <div class="mech-pages__content">
30
42
  <nav v-if="folders.length" class="mech-pages__rail">
31
43
  <button
@@ -36,7 +48,7 @@
36
48
  >
37
49
  <VIcon name="book" />
38
50
  <span class="mech-pages__rail-name">All pages</span>
39
- <span class="mech-pages__rail-count">{{ pages.length }}</span>
51
+ <span class="mech-pages__rail-count">{{ listedPages.length }}</span>
40
52
  </button>
41
53
  <button
42
54
  v-for="folder in folders"
@@ -146,6 +158,7 @@
146
158
  <script setup lang="ts">
147
159
  import { computed, inject, nextTick, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
148
160
  import VIcon from '../components/VIcon.vue'
161
+ import VSegmented from '../components/VSegmented.vue'
149
162
  import ConfirmDialog from './ConfirmDialog.vue'
150
163
  import PageFormDialog from './PageFormDialog.vue'
151
164
  import { useDialog } from '../ui/dialog'
@@ -178,9 +191,45 @@ const listEl = useTemplateRef<HTMLDivElement>('listEl')
178
191
 
179
192
  const searching = computed(() => !!query.value.trim())
180
193
 
194
+ // Generated pages (plugin `generatePages`) can be numerous — a big catalog or
195
+ // API-doc set. This toggle hides them from the list; the preference persists per
196
+ // dev-server origin and defaults to shown. `listedPages` is the filtered set that
197
+ // every count, folder, and row below derives from.
198
+ const SHOW_GENERATED_KEY = 'mech:pages:show-generated'
199
+ const showGenerated = ref(typeof localStorage === 'undefined' || localStorage.getItem(SHOW_GENERATED_KEY) !== '0')
200
+ watch(showGenerated, (value) => {
201
+ try {
202
+ localStorage.setItem(SHOW_GENERATED_KEY, value ? '1' : '0')
203
+ } catch {
204
+ /* storage unavailable */
205
+ }
206
+ })
207
+ const hasGenerated = computed(() => pages.value.some((page) => page.generated))
208
+ const listedPages = computed(() =>
209
+ showGenerated.value ? pages.value : pages.value.filter((page) => !page.generated),
210
+ )
211
+
212
+ // Sort order within each folder group. Defaults to `path` (the server's own
213
+ // order — the folder rail is path-shaped), persisted like `showGenerated`.
214
+ const SORT_KEY = 'mech:pages:sort'
215
+ const sortKey = ref<'path' | 'name'>(
216
+ (typeof localStorage !== 'undefined' && localStorage.getItem(SORT_KEY)) === 'name' ? 'name' : 'path',
217
+ )
218
+ watch(sortKey, (value) => {
219
+ try {
220
+ localStorage.setItem(SORT_KEY, value)
221
+ } catch {
222
+ /* storage unavailable */
223
+ }
224
+ })
225
+ const sortOptions = [
226
+ { value: 'path', label: 'Path' },
227
+ { value: 'name', label: 'Name' },
228
+ ]
229
+
181
230
  const folders = computed(() => {
182
231
  const counts = new Map<string, number>()
183
- for (const page of pages.value) {
232
+ for (const page of listedPages.value) {
184
233
  if (page.folderPath) counts.set(page.folderPath, (counts.get(page.folderPath) ?? 0) + 1)
185
234
  }
186
235
  return [...counts.entries()]
@@ -190,11 +239,19 @@ const folders = computed(() => {
190
239
 
191
240
  // Search is global — it ignores the folder filter so a match can never hide.
192
241
  const visible = computed(() => {
193
- if (searching.value) return filterPages(pages.value, query.value)
194
- if (activeFolder.value) return pages.value.filter((page) => page.folderPath === activeFolder.value)
195
- return pages.value
242
+ if (searching.value) return filterPages(listedPages.value, query.value)
243
+ if (activeFolder.value) return listedPages.value.filter((page) => page.folderPath === activeFolder.value)
244
+ return listedPages.value
196
245
  })
197
- const groups = computed(() => groupPagesByFolder(visible.value))
246
+ // Sort within each folder group by the chosen key (grouping is preserved).
247
+ const sorted = computed(() =>
248
+ [...visible.value].sort((a, b) =>
249
+ sortKey.value === 'name'
250
+ ? (a.name || a.path).localeCompare(b.name || b.path)
251
+ : a.path.localeCompare(b.path),
252
+ ),
253
+ )
254
+ const groups = computed(() => groupPagesByFolder(sorted.value))
198
255
  /** Flat list in render order — keyboard navigation walks this. */
199
256
  const rows = computed(() => groups.value.flatMap((group) => group.pages))
200
257
  // Inside a single folder the rail already names it; headers would be noise.
@@ -208,7 +265,9 @@ const monogram = computed(() =>
208
265
  onMounted(async () => {
209
266
  await load()
210
267
  searchInput.value?.focus()
211
- activePath.value = pages.value.some((page) => page.path === current.value)
268
+ // Highlight the current page when it's in the (possibly filtered) list, else
269
+ // the first row — a generated current page hidden by the toggle falls back.
270
+ activePath.value = rows.value.some((page) => page.path === current.value)
212
271
  ? current.value
213
272
  : (rows.value[0]?.path ?? null)
214
273
  })
@@ -447,13 +506,39 @@ async function toggleDraft(page: PageItem) {
447
506
  .mech-pages__toolbar {
448
507
  display: flex;
449
508
  gap: 8px;
450
- padding: 0 24px 14px;
509
+ padding: 0 24px 12px;
451
510
  flex: none;
452
511
  }
453
512
  .mech-pages__search-wrap {
454
513
  position: relative;
455
514
  flex: 1;
456
515
  }
516
+
517
+ // ── Filter / sort row ────────────────────────────────────────────────────────
518
+ .mech-pages__filters {
519
+ display: flex;
520
+ align-items: center;
521
+ justify-content: space-between;
522
+ gap: 12px;
523
+ padding: 0 24px 14px;
524
+ flex: none;
525
+ }
526
+ .mech-pages__sort {
527
+ display: flex;
528
+ align-items: center;
529
+ gap: 9px;
530
+ }
531
+ .mech-pages__sort-label {
532
+ font-size: 10.5px;
533
+ font-weight: 600;
534
+ text-transform: uppercase;
535
+ letter-spacing: 0.05em;
536
+ color: var(--mech-muted);
537
+ }
538
+ .mech-pages__toggle {
539
+ flex: none;
540
+ white-space: nowrap;
541
+ }
457
542
  .mech-pages__search-icon {
458
543
  position: absolute;
459
544
  left: 12px;
@@ -150,6 +150,9 @@ export interface SsrEntryOptions {
150
150
  /** Site identity (origin, name) from the plugin options — rides the bundle so
151
151
  * `mechanica export` can emit canonical URLs, sitemap.xml and robots.txt. */
152
152
  site?: { url?: string; name?: string }
153
+ /** CDN base URL for static assets (plugin `assetsUrl`) — rides the bundle so
154
+ * `mechanica export` can serve `/assets/…` and `/media/…` from it. */
155
+ assetsUrl?: string
153
156
  /** The site's locale config (multi-language) — rides the bundle so the export
154
157
  * can walk translations, and gets baked into each page's `state.locales`. */
155
158
  locales?: import('mechanica-shared').LocalesConfig | null
@@ -178,6 +181,7 @@ export function generateSsrEntry(options: SsrEntryOptions): string {
178
181
  ``,
179
182
  `registerComponents(blocksMap)`,
180
183
  `export const site = ${JSON.stringify(options.site ?? {})}`,
184
+ `export const assetsUrl = ${JSON.stringify(options.assetsUrl ?? null)}`,
181
185
  `export const locales = ${JSON.stringify(options.locales ?? null)}`,
182
186
  `export const generatedPages = ${JSON.stringify(options.generatedPages ?? [])}`,
183
187
  `export const dataEntries = getDataEntries()`,
@@ -92,6 +92,21 @@ export interface MechanicaPluginOptions {
92
92
  siteUrl?: string
93
93
  /** The site's display name — `{{ site.name }}` + WebSite JSON-LD at export. */
94
94
  siteName?: string
95
+ /**
96
+ * Base URL to serve static assets from at export — a CDN origin like
97
+ * `https://cdn.example.com`. When set, `mechanica export` rewrites both the
98
+ * build assets (`/assets/…`) and uploaded media (`/media/…`) to
99
+ * `<assetsUrl>/assets/…` and `<assetsUrl>/media/…`. Files are still emitted
100
+ * into `export/` — upload them to the CDN yourself (e.g. in CI). A trailing
101
+ * slash is ignored. `mechanica export --assets-url <origin>` overrides it.
102
+ *
103
+ * Only affects the static export; the dev server keeps serving assets
104
+ * locally. For build assets referenced from inside CSS/JS chunks (fonts,
105
+ * `url(...)` backgrounds) on a cross-origin CDN, set Vite's `base` instead —
106
+ * this option rewrites HTML asset tags and uploaded media, not chunk-internal
107
+ * references.
108
+ */
109
+ assetsUrl?: string
95
110
  /**
96
111
  * Multi-language configuration. Either a full config
97
112
  * (`{ default: 'en', all: ['en', 'ru'], labels? }`) or a bare list of codes
@@ -465,6 +480,7 @@ export function mechanica(options: MechanicaPluginOptions = {}): Plugin {
465
480
  return generateSsrEntry({
466
481
  userEntry,
467
482
  site: { url: options.siteUrl, name: options.siteName },
483
+ assetsUrl: options.assetsUrl,
468
484
  locales,
469
485
  generatedPages: await loadGeneratedPages(),
470
486
  })