@byline/admin 6.0.1 → 6.0.3

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 (92) hide show
  1. package/dist/modules/admin-preferences/index.d.ts +7 -3
  2. package/dist/modules/admin-preferences/index.js +1 -1
  3. package/dist/modules/admin-preferences/schemas.d.ts +19 -16
  4. package/dist/modules/admin-preferences/schemas.js +46 -2
  5. package/dist/modules/analytics/components/timeseries.d.ts +28 -0
  6. package/dist/modules/analytics/components/timeseries.js +107 -35
  7. package/dist/modules/analytics/components/timeseries.module.js +15 -0
  8. package/dist/modules/analytics/components/timeseries_module.css +110 -3
  9. package/package.json +7 -7
  10. package/src/modules/admin-preferences/index.ts +7 -2
  11. package/src/modules/admin-preferences/schemas.test.node.ts +74 -0
  12. package/src/modules/admin-preferences/schemas.ts +85 -8
  13. package/src/modules/analytics/components/dashboard.test.node.ts +51 -0
  14. package/src/modules/analytics/components/timeseries.module.css +176 -3
  15. package/src/modules/analytics/components/timeseries.tsx +153 -40
  16. package/dist/exports-parity.test.node.js +0 -37
  17. package/dist/fields/array/array-field.jsx +0 -209
  18. package/dist/fields/blocks/blocks-field.jsx +0 -248
  19. package/dist/fields/checkbox/checkbox-field.jsx +0 -24
  20. package/dist/fields/code/code-editor.jsx +0 -211
  21. package/dist/fields/code/code-field.jsx +0 -109
  22. package/dist/fields/column-formatter.jsx +0 -25
  23. package/dist/fields/date-time-formatter.jsx +0 -18
  24. package/dist/fields/datetime/datetime-field.jsx +0 -22
  25. package/dist/fields/draggable-context-menu.jsx +0 -50
  26. package/dist/fields/field-admin.test.node.js +0 -40
  27. package/dist/fields/field-renderer.jsx +0 -136
  28. package/dist/fields/field-services-context.jsx +0 -17
  29. package/dist/fields/file/file-field.jsx +0 -176
  30. package/dist/fields/file/file-upload-field.jsx +0 -89
  31. package/dist/fields/group/group-field.jsx +0 -46
  32. package/dist/fields/image/image-field.jsx +0 -183
  33. package/dist/fields/image/image-upload-field.jsx +0 -157
  34. package/dist/fields/local-date-time.jsx +0 -69
  35. package/dist/fields/locale-badge.jsx +0 -19
  36. package/dist/fields/numerical/numerical-field.jsx +0 -93
  37. package/dist/fields/relation/relation-column-formatter.jsx +0 -74
  38. package/dist/fields/relation/relation-display.jsx +0 -118
  39. package/dist/fields/relation/relation-field.jsx +0 -108
  40. package/dist/fields/relation/relation-many-field.jsx +0 -142
  41. package/dist/fields/relation/relation-picker.jsx +0 -240
  42. package/dist/fields/relation/relation-summary.jsx +0 -45
  43. package/dist/fields/select/select-field.jsx +0 -27
  44. package/dist/fields/sortable-item.jsx +0 -54
  45. package/dist/fields/text/text-field.jsx +0 -79
  46. package/dist/fields/text-area/text-area-field.jsx +0 -79
  47. package/dist/forms/available-locales-reconcile.test.node.js +0 -43
  48. package/dist/forms/available-locales-widget.jsx +0 -63
  49. package/dist/forms/document-actions.jsx +0 -486
  50. package/dist/forms/form-context.jsx +0 -598
  51. package/dist/forms/form-modals.jsx +0 -128
  52. package/dist/forms/form-renderer.jsx +0 -532
  53. package/dist/forms/form-status-display.jsx +0 -69
  54. package/dist/forms/navigation-guard.jsx +0 -68
  55. package/dist/forms/nested-path.test.node.js +0 -115
  56. package/dist/forms/path-widget.jsx +0 -125
  57. package/dist/forms/pending-uploads.test.node.js +0 -19
  58. package/dist/forms/repeating-items.test.node.js +0 -29
  59. package/dist/forms/scheduled-publication-control.jsx +0 -389
  60. package/dist/forms/scheduled-publication-state.test.node.js +0 -121
  61. package/dist/forms/scheduled-publication-time.test.node.js +0 -70
  62. package/dist/forms/status-transitions.test.node.js +0 -76
  63. package/dist/forms/tree-placement-widget.jsx +0 -160
  64. package/dist/forms/upload-executor.test.node.js +0 -368
  65. package/dist/forms/use-form-layout.test.node.js +0 -69
  66. package/dist/modules/admin-account/components/change-password.jsx +0 -169
  67. package/dist/modules/admin-account/components/container.jsx +0 -164
  68. package/dist/modules/admin-account/components/preferences.jsx +0 -148
  69. package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
  70. package/dist/modules/admin-account/components/update.jsx +0 -186
  71. package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
  72. package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
  73. package/dist/modules/admin-roles/components/create.jsx +0 -155
  74. package/dist/modules/admin-roles/components/permissions.jsx +0 -239
  75. package/dist/modules/admin-roles/components/update.jsx +0 -148
  76. package/dist/modules/admin-users/components/create.jsx +0 -206
  77. package/dist/modules/admin-users/components/roles.jsx +0 -126
  78. package/dist/modules/admin-users/components/set-password.jsx +0 -133
  79. package/dist/modules/admin-users/components/update.jsx +0 -215
  80. package/dist/modules/analytics/components/dashboard.jsx +0 -208
  81. package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
  82. package/dist/modules/analytics/components/timeseries.jsx +0 -193
  83. package/dist/modules/auth/components/sign-in-form.jsx +0 -89
  84. package/dist/modules/auth/safe-redirect.test.node.js +0 -40
  85. package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
  86. package/dist/presentation/group.jsx +0 -26
  87. package/dist/presentation/row.jsx +0 -23
  88. package/dist/presentation/tabs.jsx +0 -39
  89. package/dist/services/admin-services-context.jsx +0 -17
  90. package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
  91. package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
  92. package/dist/widgets/status-badge/status-badge.jsx +0 -46
@@ -11,11 +11,15 @@
11
11
  *
12
12
  * Self-service like `@byline/admin/admin-account`: the actor IS the
13
13
  * target, and there is no ability gate — authn-only. The `scope` string
14
- * (e.g. `collections.docs.list`) is the generality lever: new admin
15
- * surfaces claim their own scopes with no schema change.
14
+ * (e.g. `collections.docs.list`, `analytics.dashboard`) is the
15
+ * generality lever: a new admin surface claims its own scope by
16
+ * registering a value schema for it in `schemas.ts`, which keeps each
17
+ * family's payload strictly validated and unregistered scopes
18
+ * unwritable. Storage itself is untouched — scopes share one JSON
19
+ * key-value table.
16
20
  */
17
21
  export { getPreferenceCommand, setPreferenceCommand } from './commands.js';
18
- export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema, } from './schemas.js';
22
+ export { analyticsViewPreferenceValueSchema, getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema, } from './schemas.js';
19
23
  export { AdminPreferencesService } from './service.js';
20
24
  export type { AdminPreferencesCommandDeps } from './commands.js';
21
25
  export type { AdminPreferencesRepository, AdminUserPreferenceRow, } from './repository.js';
@@ -1,3 +1,3 @@
1
1
  export { getPreferenceCommand, setPreferenceCommand } from "./commands.js";
2
- export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema } from "./schemas.js";
2
+ export { analyticsViewPreferenceValueSchema, getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema } from "./schemas.js";
3
3
  export { AdminPreferencesService } from "./service.js";
@@ -5,17 +5,6 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- /**
9
- * Zod schemas for the admin-preferences commands.
10
- *
11
- * Self-service, like admin-account: none of the request schemas accept a
12
- * user id — the command resolves the target from `actor.id`.
13
- *
14
- * The `value` payload is validated against the list-view shape because
15
- * that is the only registered scope family today
16
- * (`collections.<path>.list`). When a second scope family arrives, this
17
- * becomes a scope-keyed selection of value schemas.
18
- */
19
8
  import { z } from 'zod';
20
9
  /** Dot-separated segment key, e.g. `collections.docs.list`. */
21
10
  export declare const preferenceScopeSchema: z.ZodString;
@@ -29,17 +18,31 @@ export declare const listViewPreferenceValueSchema: z.ZodObject<{
29
18
  order: z.ZodOptional<z.ZodString>;
30
19
  desc: z.ZodOptional<z.ZodBoolean>;
31
20
  }, z.core.$strict>;
21
+ /**
22
+ * Sticky analytics-dashboard keys. The period list is sourced from
23
+ * `@byline/analytics/config` rather than restated, so the stored value
24
+ * can only ever be one the dashboard actually offers — adding a sixth
25
+ * period there admits it here with no edit.
26
+ *
27
+ * Note the stored `period` is the *parsed* value (`30`, `'ytd'`), not
28
+ * the `?period=30` URL string the route validates on the way in.
29
+ */
30
+ export declare const analyticsViewPreferenceValueSchema: z.ZodObject<{
31
+ period: z.ZodUnion<readonly [z.ZodLiteral<7>, z.ZodLiteral<30>, z.ZodLiteral<90>, z.ZodLiteral<"ytd">, z.ZodLiteral<"all">]>;
32
+ }, z.core.$strict>;
33
+ /**
34
+ * The value schema a scope selects, or `undefined` when the scope
35
+ * belongs to no registered family. Exported so callers (and tests) can
36
+ * ask the same question the write path asks.
37
+ */
38
+ export declare function resolvePreferenceValueSchema(scope: string): z.ZodType | undefined;
32
39
  export declare const getPreferenceRequestSchema: z.ZodObject<{
33
40
  scope: z.ZodString;
34
41
  }, z.core.$strip>;
35
42
  export type GetPreferenceRequest = z.infer<typeof getPreferenceRequestSchema>;
36
43
  export declare const setPreferenceRequestSchema: z.ZodObject<{
37
44
  scope: z.ZodString;
38
- value: z.ZodObject<{
39
- page_size: z.ZodOptional<z.ZodNumber>;
40
- order: z.ZodOptional<z.ZodString>;
41
- desc: z.ZodOptional<z.ZodBoolean>;
42
- }, z.core.$strict>;
45
+ value: z.ZodRecord<z.ZodString, z.ZodUnknown>;
43
46
  }, z.core.$strip>;
44
47
  export type SetPreferenceRequest = z.infer<typeof setPreferenceRequestSchema>;
45
48
  /** `value` is `null` when the user has no stored preference for the scope. */
@@ -1,3 +1,4 @@
1
+ import { ANALYTICS_DASHBOARD_PERIODS } from "@byline/analytics/config";
1
2
  import { z } from "zod";
2
3
  const preferenceScopeSchema = z.string().min(1).max(255).regex(/^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i, {
3
4
  message: 'scope must be dot-separated segments of [a-z0-9_-]'
@@ -9,15 +10,58 @@ const listViewPreferenceValueSchema = z.object({
9
10
  }).strict().refine((v)=>Object.keys(v).length > 0, {
10
11
  message: 'value cannot be empty'
11
12
  });
13
+ const analyticsViewPreferenceValueSchema = z.object({
14
+ period: z.union([
15
+ z.literal(ANALYTICS_DASHBOARD_PERIODS["0"]),
16
+ z.literal(ANALYTICS_DASHBOARD_PERIODS["1"]),
17
+ z.literal(ANALYTICS_DASHBOARD_PERIODS["2"]),
18
+ z.literal(ANALYTICS_DASHBOARD_PERIODS["3"]),
19
+ z.literal(ANALYTICS_DASHBOARD_PERIODS["4"])
20
+ ])
21
+ }).strict();
22
+ const PREFERENCE_VALUE_SCHEMAS = [
23
+ {
24
+ matches: (scope)=>{
25
+ const segments = scope.split('.');
26
+ return 3 === segments.length && 'collections' === segments[0] && 'list' === segments[2];
27
+ },
28
+ schema: listViewPreferenceValueSchema
29
+ },
30
+ {
31
+ matches: (scope)=>'analytics.dashboard' === scope,
32
+ schema: analyticsViewPreferenceValueSchema
33
+ }
34
+ ];
35
+ function resolvePreferenceValueSchema(scope) {
36
+ return PREFERENCE_VALUE_SCHEMAS.find((family)=>family.matches(scope))?.schema;
37
+ }
12
38
  const getPreferenceRequestSchema = z.object({
13
39
  scope: preferenceScopeSchema
14
40
  });
15
41
  const setPreferenceRequestSchema = z.object({
16
42
  scope: preferenceScopeSchema,
17
- value: listViewPreferenceValueSchema
43
+ value: z.record(z.string(), z.unknown())
44
+ }).superRefine((input, ctx)=>{
45
+ const schema = resolvePreferenceValueSchema(input.scope);
46
+ if (null == schema) return void ctx.addIssue({
47
+ code: 'custom',
48
+ path: [
49
+ 'scope'
50
+ ],
51
+ message: 'scope does not belong to a registered preference family'
52
+ });
53
+ const result = schema.safeParse(input.value);
54
+ if (result.success) return;
55
+ for (const issue of result.error.issues)ctx.addIssue({
56
+ ...issue,
57
+ path: [
58
+ 'value',
59
+ ...issue.path
60
+ ]
61
+ });
18
62
  });
19
63
  const preferenceResponseSchema = z.object({
20
64
  scope: z.string(),
21
65
  value: z.record(z.string(), z.unknown()).nullable()
22
66
  });
23
- export { getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, setPreferenceRequestSchema };
67
+ export { analyticsViewPreferenceValueSchema, getPreferenceRequestSchema, listViewPreferenceValueSchema, preferenceResponseSchema, preferenceScopeSchema, resolvePreferenceValueSchema, setPreferenceRequestSchema };
@@ -51,6 +51,34 @@ export declare function resolveAnalyticsChartGranularity(period: AnalyticsDashbo
51
51
  export declare function bucketAnalyticsTimeseries(days: readonly AnalyticsSummaryDay[], granularity: AnalyticsChartGranularity): readonly AnalyticsChartBucket[];
52
52
  /** Project reporting buckets into column geometry. */
53
53
  export declare function buildAnalyticsColumns(buckets: readonly Pick<AnalyticsChartBucket, 'views' | 'visitors'>[]): readonly AnalyticsColumn[];
54
+ /**
55
+ * Where the hover card sits relative to the plot box.
56
+ *
57
+ * `leftPercent` / `topPercent` are percentages of the plot rather than user
58
+ * -space units because the card is HTML laid over a stretched SVG: the plot
59
+ * is drawn `preserveAspectRatio="none"`, so anything measured in viewBox
60
+ * units would skew with the container width (the same reason the axis labels
61
+ * are HTML). Percentages survive the stretch.
62
+ */
63
+ export interface AnalyticsHoverCardPlacement {
64
+ /** Horizontal centre of the hovered column, as a percentage of plot width. */
65
+ leftPercent: number;
66
+ /** Top edge of the hovered column, as a percentage of plot height. */
67
+ topPercent: number;
68
+ /**
69
+ * How the card lines up with `leftPercent`. Columns near a plot edge anchor
70
+ * that edge instead of centring, so the card never overhangs the chart.
71
+ */
72
+ align: 'start' | 'center' | 'end';
73
+ /**
74
+ * Which side of the column top the card occupies. A column tall enough that
75
+ * a card above it would clip the top of the plot takes the card below
76
+ * instead.
77
+ */
78
+ side: 'above' | 'below';
79
+ }
80
+ /** Project one column's geometry into hover-card placement. */
81
+ export declare function resolveHoverCardPlacement(column: AnalyticsColumn): AnalyticsHoverCardPlacement;
54
82
  export interface AnalyticsTimeseriesProps {
55
83
  days: readonly AnalyticsSummaryDay[];
56
84
  granularity: AnalyticsChartGranularity;
@@ -51,6 +51,18 @@ function buildAnalyticsColumns(buckets) {
51
51
  };
52
52
  });
53
53
  }
54
+ const CARD_EDGE_MARGIN = 15;
55
+ const CARD_FLIP_THRESHOLD = 45;
56
+ function resolveHoverCardPlacement(column) {
57
+ const leftPercent = (column.hitX + column.hitWidth / 2) / VIEWBOX_WIDTH * 100;
58
+ const topPercent = column.y / VIEWBOX_HEIGHT * 100;
59
+ return {
60
+ leftPercent,
61
+ topPercent,
62
+ align: leftPercent < CARD_EDGE_MARGIN ? 'start' : leftPercent > 100 - CARD_EDGE_MARGIN ? 'end' : 'center',
63
+ side: topPercent < CARD_FLIP_THRESHOLD ? 'below' : 'above'
64
+ };
65
+ }
54
66
  function AnalyticsTimeseries({ days, granularity, locale }) {
55
67
  const { t } = useTranslation('byline-admin');
56
68
  const [hovered, setHovered] = useState(null);
@@ -76,6 +88,13 @@ function AnalyticsTimeseries({ days, granularity, locale }) {
76
88
  const formatBucket = (bucket)=>1 === bucket.dayCount ? formatDay(bucket.from) : `${formatDay(bucket.from)} – ${formatDay(bucket.to)}`;
77
89
  const visitorLabel = (bucket)=>'day' === bucket.granularity ? t('analytics.stats.dailyUniques') : t('analytics.stats.summedDailyUniques');
78
90
  const active = null == hovered ? void 0 : buckets[hovered];
91
+ const activePlacement = null == hovered || null == columns[hovered] ? void 0 : resolveHoverCardPlacement(columns[hovered]);
92
+ const lastShown = useRef(null);
93
+ if (null != active && null != activePlacement) lastShown.current = {
94
+ bucket: active,
95
+ placement: activePlacement
96
+ };
97
+ const shown = lastShown.current;
79
98
  const readBucket = (bucket)=>[
80
99
  formatBucket(bucket),
81
100
  `${t('analytics.stats.views')} ${numbers.format(bucket.views)}`,
@@ -116,46 +135,99 @@ function AnalyticsTimeseries({ days, granularity, locale }) {
116
135
  onKeyDown: trackKey,
117
136
  onBlur: ()=>setHovered(null),
118
137
  children: [
119
- /*#__PURE__*/ jsxs("svg", {
120
- ref: plot,
121
- className: clsx('byline-analytics-chart', timeseries_module.chart),
122
- viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
123
- preserveAspectRatio: "none",
124
- "aria-hidden": "true",
138
+ /*#__PURE__*/ jsxs("div", {
139
+ className: clsx('byline-analytics-plot', timeseries_module.plot),
125
140
  children: [
126
- /*#__PURE__*/ jsx("title", {
127
- children: t('analytics.chart.views')
141
+ /*#__PURE__*/ jsxs("svg", {
142
+ ref: plot,
143
+ className: clsx('byline-analytics-chart', timeseries_module.chart),
144
+ viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
145
+ preserveAspectRatio: "none",
146
+ "aria-hidden": "true",
147
+ children: [
148
+ /*#__PURE__*/ jsx("title", {
149
+ children: t('analytics.chart.views')
150
+ }),
151
+ [
152
+ 0.5,
153
+ 1
154
+ ].map((fraction)=>/*#__PURE__*/ jsx("line", {
155
+ className: timeseries_module.gridline,
156
+ x1: 0,
157
+ x2: VIEWBOX_WIDTH,
158
+ y1: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
159
+ y2: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
160
+ vectorEffect: "non-scaling-stroke"
161
+ }, fraction)),
162
+ columns.map((column)=>/*#__PURE__*/ jsxs("g", {
163
+ className: clsx(timeseries_module.column, hovered === column.index && timeseries_module.columnActive),
164
+ children: [
165
+ /*#__PURE__*/ jsx("rect", {
166
+ className: timeseries_module.views,
167
+ x: column.x,
168
+ width: column.width,
169
+ y: column.y,
170
+ height: column.height
171
+ }),
172
+ /*#__PURE__*/ jsx("rect", {
173
+ className: timeseries_module.visitors,
174
+ x: column.insetX,
175
+ width: column.insetWidth,
176
+ y: column.insetY,
177
+ height: column.insetHeight
178
+ })
179
+ ]
180
+ }, buckets[column.index]?.from))
181
+ ]
128
182
  }),
129
- [
130
- 0.5,
131
- 1
132
- ].map((fraction)=>/*#__PURE__*/ jsx("line", {
133
- className: timeseries_module.gridline,
134
- x1: 0,
135
- x2: VIEWBOX_WIDTH,
136
- y1: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
137
- y2: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
138
- vectorEffect: "non-scaling-stroke"
139
- }, fraction)),
140
- columns.map((column)=>/*#__PURE__*/ jsxs("g", {
141
- className: clsx(timeseries_module.column, hovered === column.index && timeseries_module.columnActive),
183
+ buckets.length > 0 && /*#__PURE__*/ jsx("div", {
184
+ className: clsx('byline-analytics-hovercard', timeseries_module.hoverCard, null != shown && timeseries_module[shown.placement.align], null != shown && ('below' === shown.placement.side ? timeseries_module.below : timeseries_module.above), null != active && timeseries_module.hoverCardVisible),
185
+ style: {
186
+ left: `${shown?.placement.leftPercent ?? 50}%`,
187
+ top: `${shown?.placement.topPercent ?? 100}%`
188
+ },
189
+ "aria-hidden": "true",
190
+ children: null != shown && /*#__PURE__*/ jsxs(Fragment, {
142
191
  children: [
143
- /*#__PURE__*/ jsx("rect", {
144
- className: timeseries_module.views,
145
- x: column.x,
146
- width: column.width,
147
- y: column.y,
148
- height: column.height
192
+ /*#__PURE__*/ jsx("span", {
193
+ className: timeseries_module.hoverCardDate,
194
+ children: formatBucket(shown.bucket)
149
195
  }),
150
- /*#__PURE__*/ jsx("rect", {
151
- className: timeseries_module.visitors,
152
- x: column.insetX,
153
- width: column.insetWidth,
154
- y: column.insetY,
155
- height: column.insetHeight
196
+ /*#__PURE__*/ jsxs("span", {
197
+ className: timeseries_module.hoverCardRow,
198
+ children: [
199
+ /*#__PURE__*/ jsx("span", {
200
+ className: clsx(timeseries_module.keyLine, timeseries_module.keyViews)
201
+ }),
202
+ /*#__PURE__*/ jsx("span", {
203
+ className: timeseries_module.hoverCardLabel,
204
+ children: t('analytics.stats.views')
205
+ }),
206
+ /*#__PURE__*/ jsx("span", {
207
+ className: timeseries_module.hoverCardValue,
208
+ children: numbers.format(shown.bucket.views)
209
+ })
210
+ ]
211
+ }),
212
+ /*#__PURE__*/ jsxs("span", {
213
+ className: timeseries_module.hoverCardRow,
214
+ children: [
215
+ /*#__PURE__*/ jsx("span", {
216
+ className: clsx(timeseries_module.keyLine, timeseries_module.keyVisitors)
217
+ }),
218
+ /*#__PURE__*/ jsx("span", {
219
+ className: timeseries_module.hoverCardLabel,
220
+ children: visitorLabel(shown.bucket)
221
+ }),
222
+ /*#__PURE__*/ jsx("span", {
223
+ className: timeseries_module.hoverCardValue,
224
+ children: numbers.format(shown.bucket.visitors)
225
+ })
226
+ ]
156
227
  })
157
228
  ]
158
- }, buckets[column.index]?.from))
229
+ })
230
+ })
159
231
  ]
160
232
  }),
161
233
  /*#__PURE__*/ jsxs("div", {
@@ -250,4 +322,4 @@ function combineRows(from, to, granularity, rows) {
250
322
  downloads
251
323
  };
252
324
  }
253
- export { AnalyticsTimeseries, bucketAnalyticsTimeseries, buildAnalyticsColumns, resolveAnalyticsChartGranularity };
325
+ export { AnalyticsTimeseries, bucketAnalyticsTimeseries, buildAnalyticsColumns, resolveAnalyticsChartGranularity, resolveHoverCardPlacement };
@@ -1,7 +1,22 @@
1
1
  import "./timeseries_module.css";
2
2
  const timeseries_module = {
3
3
  root: "root-L2qL4D",
4
+ plot: "plot-hcQkYI",
4
5
  chart: "chart-giYTrx",
6
+ hoverCard: "hoverCard-riiLLo",
7
+ hoverCardVisible: "hoverCardVisible-mYSsIo",
8
+ center: "center-WOF43a",
9
+ start: "start-rwR0vM",
10
+ end: "end-dDnFtK",
11
+ above: "above-V9eoE9",
12
+ below: "below-BMn9q_",
13
+ hoverCardDate: "hoverCardDate-Td91gd",
14
+ hoverCardRow: "hoverCardRow-fMGuNA",
15
+ keyLine: "keyLine-wvPvBg",
16
+ keyViews: "keyViews-G8H3zC",
17
+ keyVisitors: "keyVisitors-P1JVE9",
18
+ hoverCardLabel: "hoverCardLabel-JYa1xk",
19
+ hoverCardValue: "hoverCardValue-m_cylC",
5
20
  gridline: "gridline-R_wAwP",
6
21
  column: "column-_Cw2Ua",
7
22
  views: "views-WHbm44",
@@ -10,12 +10,115 @@
10
10
  outline-offset: 4px;
11
11
  }
12
12
 
13
+ :is(.plot-hcQkYI, .byline-analytics-plot) {
14
+ position: relative;
15
+ }
16
+
13
17
  :is(.chart-giYTrx, .byline-analytics-chart) {
14
18
  width: 100%;
15
19
  height: 11rem;
16
20
  display: block;
17
21
  }
18
22
 
23
+ :is(.hoverCard-riiLLo, .byline-analytics-hovercard) {
24
+ z-index: 1;
25
+ background-color: var(--surface-panel-elevated);
26
+ border-width: var(--border-width-thin);
27
+ border-style: var(--border-style-solid);
28
+ border-color: var(--surface-panel-border);
29
+ border-radius: var(--border-radius-sm);
30
+ box-shadow: var(--shadow-sm);
31
+ font-variant-numeric: tabular-nums;
32
+ white-space: nowrap;
33
+ pointer-events: none;
34
+ opacity: 0;
35
+ gap: .125rem .5rem;
36
+ padding: .4rem .55rem;
37
+ font-size: .6875rem;
38
+ line-height: 1.35;
39
+ transition: opacity .16s ease-in, translate .16s ease-in;
40
+ display: grid;
41
+ position: absolute;
42
+ translate: 0 6px;
43
+ }
44
+
45
+ .hoverCardVisible-mYSsIo {
46
+ opacity: 1;
47
+ transition: opacity .24s ease-out, translate .24s ease-out;
48
+ translate: 0;
49
+ }
50
+
51
+ .hoverCard-riiLLo.center-WOF43a {
52
+ transform: translateX(-50%);
53
+ }
54
+
55
+ .hoverCard-riiLLo.start-rwR0vM {
56
+ transform: translateX(-.5rem);
57
+ }
58
+
59
+ .hoverCard-riiLLo.end-dDnFtK {
60
+ transform: translateX(calc(.5rem - 100%));
61
+ }
62
+
63
+ .hoverCard-riiLLo.above-V9eoE9 {
64
+ margin-top: -.5rem;
65
+ }
66
+
67
+ .hoverCard-riiLLo.above-V9eoE9.center-WOF43a {
68
+ transform: translate(-50%, -100%);
69
+ }
70
+
71
+ .hoverCard-riiLLo.above-V9eoE9.start-rwR0vM {
72
+ transform: translate(-.5rem, -100%);
73
+ }
74
+
75
+ .hoverCard-riiLLo.above-V9eoE9.end-dDnFtK {
76
+ transform: translate(calc(.5rem - 100%), -100%);
77
+ }
78
+
79
+ .hoverCard-riiLLo.below-BMn9q_ {
80
+ margin-top: .5rem;
81
+ }
82
+
83
+ .hoverCardDate-Td91gd {
84
+ color: var(--text);
85
+ letter-spacing: .01em;
86
+ font-weight: 600;
87
+ }
88
+
89
+ .hoverCardRow-fMGuNA {
90
+ grid-template-columns: .625rem 1fr auto;
91
+ align-items: center;
92
+ gap: .4rem;
93
+ display: grid;
94
+ }
95
+
96
+ .keyLine-wvPvBg {
97
+ border-radius: 1px;
98
+ width: .625rem;
99
+ height: 2px;
100
+ display: inline-block;
101
+ }
102
+
103
+ .keyViews-G8H3zC {
104
+ opacity: .62;
105
+ background-color: #38bdf8;
106
+ }
107
+
108
+ .keyVisitors-P1JVE9 {
109
+ background-color: #7c3aed;
110
+ }
111
+
112
+ .hoverCardLabel-JYa1xk {
113
+ color: var(--gray-500);
114
+ }
115
+
116
+ .hoverCardValue-m_cylC {
117
+ color: var(--text);
118
+ text-align: right;
119
+ font-weight: 600;
120
+ }
121
+
19
122
  .gridline-R_wAwP {
20
123
  stroke: var(--gray-200);
21
124
  stroke-width: 1px;
@@ -96,7 +199,7 @@
96
199
  stroke: var(--gray-700);
97
200
  }
98
201
 
99
- :is(:is([data-theme="dark"], .dark) .axis-fBshTw, :is([data-theme="dark"], .dark) .byline-analytics-axis, :is([data-theme="dark"], .dark) .readout-L9jEpz, :is([data-theme="dark"], .dark) .byline-analytics-readout) {
202
+ :is(:is([data-theme="dark"], .dark) .axis-fBshTw, :is([data-theme="dark"], .dark) .byline-analytics-axis, :is([data-theme="dark"], .dark) .readout-L9jEpz, :is([data-theme="dark"], .dark) .byline-analytics-readout, :is([data-theme="dark"], .dark) .hoverCardLabel-JYa1xk) {
100
203
  color: var(--gray-400);
101
204
  }
102
205
 
@@ -104,7 +207,7 @@
104
207
  fill: #a78bfa;
105
208
  }
106
209
 
107
- :is([data-theme="dark"], .dark) .swatchVisitors-O3BWz6 {
210
+ :is([data-theme="dark"], .dark) .swatchVisitors-O3BWz6, :is([data-theme="dark"], .dark) .keyVisitors-P1JVE9 {
108
211
  background-color: #a78bfa;
109
212
  }
110
213
 
@@ -113,9 +216,13 @@
113
216
  }
114
217
 
115
218
  @media (prefers-reduced-motion: reduce) {
116
- .column-_Cw2Ua, .views-WHbm44 {
219
+ .column-_Cw2Ua, .views-WHbm44, .hoverCard-riiLLo, .hoverCardVisible-mYSsIo {
117
220
  transition: none;
118
221
  }
222
+
223
+ .hoverCard-riiLLo, .hoverCardVisible-mYSsIo {
224
+ translate: none;
225
+ }
119
226
  }
120
227
 
121
228
  @media (max-width: 720px) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "6.0.1",
5
+ "version": "6.0.3",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -183,12 +183,12 @@
183
183
  "react-diff-viewer-continued": "^4.4.0",
184
184
  "uuid": "^14.0.2",
185
185
  "zod": "^4.6.2",
186
- "@byline/auth": "6.0.1",
187
- "@byline/analytics": "6.0.1",
188
- "@byline/analytics-agent": "6.0.1",
189
- "@byline/core": "6.0.1",
190
- "@byline/i18n": "6.0.1",
191
- "@byline/ui": "6.0.1"
186
+ "@byline/analytics": "6.0.3",
187
+ "@byline/analytics-agent": "6.0.3",
188
+ "@byline/core": "6.0.3",
189
+ "@byline/i18n": "6.0.3",
190
+ "@byline/ui": "6.0.3",
191
+ "@byline/auth": "6.0.3"
192
192
  },
193
193
  "peerDependencies": {
194
194
  "react": "^19.0.0",
@@ -12,12 +12,17 @@
12
12
  *
13
13
  * Self-service like `@byline/admin/admin-account`: the actor IS the
14
14
  * target, and there is no ability gate — authn-only. The `scope` string
15
- * (e.g. `collections.docs.list`) is the generality lever: new admin
16
- * surfaces claim their own scopes with no schema change.
15
+ * (e.g. `collections.docs.list`, `analytics.dashboard`) is the
16
+ * generality lever: a new admin surface claims its own scope by
17
+ * registering a value schema for it in `schemas.ts`, which keeps each
18
+ * family's payload strictly validated and unregistered scopes
19
+ * unwritable. Storage itself is untouched — scopes share one JSON
20
+ * key-value table.
17
21
  */
18
22
 
19
23
  export { getPreferenceCommand, setPreferenceCommand } from './commands.js'
20
24
  export {
25
+ analyticsViewPreferenceValueSchema,
21
26
  getPreferenceRequestSchema,
22
27
  listViewPreferenceValueSchema,
23
28
  preferenceResponseSchema,