@devslab/site-kit 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devslab/site-kit",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Strict i18n, SEO/GEO, and Solid public-site shells for DevsLab products",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -65,7 +65,7 @@
65
65
  "./fonts/*": "./fonts/*"
66
66
  },
67
67
  "dependencies": {
68
- "@devslab/dds-solid": "0.16.0"
68
+ "@devslab/dds-solid": "0.17.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@tanstack/solid-router": "1.170.30",
@@ -134,10 +134,17 @@ export interface ConsentManager {
134
134
  }
135
135
  export declare function createConsentManager(options: ConsentManagerOptions): ConsentManager;
136
136
 
137
+ /**
138
+ * The bar is short: `title`, `body` (what, why, that it is optional) and `learnMore`, a link to the
139
+ * privacy policy's analytics section (ConsentBanner's `learnMoreHref`), which carries the full disclosure.
140
+ * The settings dialog: one line each for necessary and analytics, a policy link, cancel and save.
141
+ */
137
142
  export interface ConsentMessages {
138
143
  regionLabel: string;
139
144
  title: string;
140
145
  body: string;
146
+ /** "자세히 보기" / "Learn more": the bar's link to the policy section. */
147
+ learnMore: string;
141
148
  privacyLink: string;
142
149
  acceptAll: string;
143
150
  rejectAll: string;
@@ -152,6 +159,8 @@ export interface ConsentMessages {
152
159
  analyticsBody: string;
153
160
  analyticsSwitch: string;
154
161
  save: string;
162
+ /** Closes the settings dialog without saving, like ✕ and Escape. */
163
+ cancel: string;
155
164
  close: string;
156
165
  saved: string;
157
166
  trigger: string;
@@ -518,25 +518,33 @@ export function createConsentManager(options) {
518
518
  });
519
519
  }
520
520
 
521
- /** Default strings, Korean. Products pass these (or their own) to the banner. */
521
+ /**
522
+ * Default strings, Korean. Products pass these (or their own) to the banner.
523
+ * Short on purpose (owner decision, 2026-10-06): the bar says what and why and
524
+ * that it is optional; "자세히 보기" links to the product's privacy policy,
525
+ * whose analytics section carries the full disclosure (Google Analytics,
526
+ * recipient, overseas transfer, retention, no advertising, withdrawal).
527
+ */
522
528
  export const CONSENT_MESSAGES_KO = Object.freeze({
523
529
  regionLabel: "쿠키 동의",
524
- title: "방문 통계에 쿠키를 써도 될까요?",
525
- body: "동의하시면 Google Analytics로 어떤 페이지를 얼마나 보는지 통계를 냅니다. 데이터는 Google LLC(미국)로 전송되어 14개월 동안 보관되고, 광고에는 쓰지 않습니다. 동의하지 않아도 사이트의 모든 기능을 그대로 쓸 수 있고, 바닥글의 ‘쿠키 설정’에서 언제든 바꿀 수 있습니다.",
530
+ title: "이용 통계 수집 동의 (선택)",
531
+ body: "서비스를 더 낫게 만들기 위해 이용 통계를 수집합니다. 동의는 선택이며, 동의하지 않아도 모든 기능을 쓸 수 있습니다.",
532
+ learnMore: "자세히 보기",
526
533
  privacyLink: "개인정보처리방침",
527
534
  acceptAll: "모두 허용",
528
535
  rejectAll: "거부",
529
536
  settings: "설정",
530
537
  dismiss: "선택하지 않고 닫기",
531
538
  settingsTitle: "쿠키 설정",
532
- settingsIntro: "필수 쿠키는 사이트가 동작하는 데 필요해 항상 사용합니다. 방문 통계는 동의하실 때만 사용하며, 여기서 언제든 바꾸거나 철회할 수 있습니다.",
539
+ settingsIntro: "바닥글의 ‘쿠키 설정’에서 언제든 다시 바꿀 수 있습니다.",
533
540
  necessaryTitle: "필수",
534
- necessaryBody: "서비스 제공과 보안, 법령상 의무를 위해 꼭 필요해 거부할 수 없는 쿠키입니다. 이 선택을 기억하는 쿠키도 여기에 속합니다.",
541
+ necessaryBody: "사이트가 동작하는 데 꼭 필요해 항상 사용합니다.",
535
542
  necessaryStatus: "항상 사용",
536
- analyticsTitle: "방문 통계 (선택)",
537
- analyticsBody: "Google Analytics 4로 방문한 페이지, 머문 시간, 기기·브라우저 종류를 가명 식별자와 함께 수집해 통계를 냅니다. 받는 곳: Google LLC(미국으로 국외 이전). 보관 기간: 14개월. 광고에는 쓰지 않습니다.",
538
- analyticsSwitch: "방문 통계 허용",
543
+ analyticsTitle: "이용 통계 (선택)",
544
+ analyticsBody: "서비스를 개선하는 데 쓰는 이용 통계입니다.",
545
+ analyticsSwitch: "이용 통계 수집 허용",
539
546
  save: "선택 저장",
547
+ cancel: "취소",
540
548
  close: "닫기",
541
549
  saved: "쿠키 설정을 저장했습니다.",
542
550
  trigger: "쿠키 설정",
@@ -545,22 +553,24 @@ export const CONSENT_MESSAGES_KO = Object.freeze({
545
553
  /** Default strings, English. */
546
554
  export const CONSENT_MESSAGES_EN = Object.freeze({
547
555
  regionLabel: "Cookie consent",
548
- title: "May we use cookies for visit statistics?",
549
- body: "If you agree, Google Analytics counts which pages are viewed and for how long. The data goes to Google LLC in the United States, is kept for 14 months, and is never used for ads. Everything on this site works without it, and you can change your choice at any time under “Cookie settings” in the footer.",
556
+ title: "Analytics (optional)",
557
+ body: "We collect usage statistics to improve the service. It's optional, and everything works without it.",
558
+ learnMore: "Learn more",
550
559
  privacyLink: "Privacy policy",
551
560
  acceptAll: "Accept all",
552
561
  rejectAll: "Reject",
553
562
  settings: "Settings",
554
563
  dismiss: "Close without choosing",
555
564
  settingsTitle: "Cookie settings",
556
- settingsIntro: "Necessary cookies keep the site working and are always on. Visit statistics run only with your consent, and you can change or withdraw it here at any time.",
565
+ settingsIntro: "You can change this at any time under “Cookie settings” in the footer.",
557
566
  necessaryTitle: "Necessary",
558
- necessaryBody: "Needed to provide the service, keep it secure and meet legal obligations, so they cannot be turned off. The cookie that remembers this choice is one of them.",
567
+ necessaryBody: "Needed for the site to work, so always on.",
559
568
  necessaryStatus: "Always on",
560
- analyticsTitle: "Visit statistics (optional)",
561
- analyticsBody: "Google Analytics 4 records the pages you view, time spent, and device and browser type under a pseudonymous ID to produce statistics. Recipient: Google LLC (transferred to the United States). Kept for 14 months. Never used for ads.",
562
- analyticsSwitch: "Allow visit statistics",
569
+ analyticsTitle: "Analytics (optional)",
570
+ analyticsBody: "Usage statistics that help us improve the service.",
571
+ analyticsSwitch: "Allow analytics",
563
572
  save: "Save choices",
573
+ cancel: "Cancel",
564
574
  close: "Close",
565
575
  saved: "Your cookie settings are saved.",
566
576
  trigger: "Cookie settings",
@@ -1,3 +1,4 @@
1
+ // Mirrors index.mjs statement for statement: each module re-exported whole.
1
2
  export * from "./locales.mjs";
2
3
  export * from "./catalog.mjs";
3
4
  export * from "./seo.mjs";
@@ -1,11 +1,15 @@
1
- export { FAMILY_LOCALES, LOCALES, canonicalLocale, defineLocaleRegistry, localeAttributes, resolveLocale } from "./locales.mjs";
2
- export { CatalogValidationError, createTranslator, validateCatalogs } from "./catalog.mjs";
3
- export { BRAND_ICON_FILES, ROBOTS_USER_AGENTS, brandIconLinks, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
4
- export { VerifiedFactRegistry, buildVerifiedJsonLd, renderLlmsTxt } from "./geo.mjs";
5
- export { definePublisher, buildPublisher, serializeJsonLd, renderPublisherHtml } from "./publisher.mjs";
6
- export { GTM_CONTAINER_ID_PATTERN, GTM_CSP_SOURCES, gtmHeadScript, gtmNoscriptIframe } from "./gtm.mjs";
7
- export { FAMILY_FONT_PRELOAD_FILE, fontPreloadLinks } from "./fonts.mjs";
8
- export {
9
- CONSENT_COOKIE_NAME, CONSENT_MESSAGES_EN, CONSENT_MESSAGES_KO, CONSENT_MODE_DEFAULTS, CONSENT_RESPONSE_HEADERS, analyticsConsented, consentCookieGrantsAnalytics,
10
- consentHeadScript, createConsentManager, isSameOriginRequest, parseConsentRecord, postConsentRecord, readConsentCookie,
11
- } from "./consent.mjs";
1
+ // The root barrel re-exports each module whole, statement for statement the
2
+ // same as index.d.mts. It used to name every export by hand, and the consent
3
+ // list fell behind its own declarations: CONSENT_RECORD_MAX_BYTES and nine
4
+ // other names type-checked from "@devslab/site-kit" and were undefined at
5
+ // runtime. A module that should stay off the root (flags, devslab) is simply
6
+ // not listed here; scripts/verify-site-kit-release.mjs checks the packed
7
+ // tarball's declared exports against what each entry point really exports.
8
+ export * from "./locales.mjs";
9
+ export * from "./catalog.mjs";
10
+ export * from "./seo.mjs";
11
+ export * from "./geo.mjs";
12
+ export * from "./publisher.mjs";
13
+ export * from "./gtm.mjs";
14
+ export * from "./fonts.mjs";
15
+ export * from "./consent.mjs";
package/styles.css CHANGED
@@ -110,22 +110,28 @@
110
110
  .site-locale-flag__option[aria-current="true"] { font-weight: 700; }
111
111
  @media (max-width: 720px) { .site-locale-flag__list { position: static; inline-size: 100%; max-block-size: none; box-shadow: none; } }
112
112
 
113
- /* Consent (D-034): a bottom bar with three equal choices, and a settings dialog.
114
- The three buttons share one tone and one grid track width, so no choice is
115
- drawn heavier than another. */
113
+ /* Consent (D-034): a bottom bar with three equal choices, and a settings dialog
114
+ with two (cancel, save). Each set shares one tone and one grid track width,
115
+ so no choice is drawn heavier than another. Products bring their own, longer labels, so the
116
+ layout follows the bar's own width (a container), in three steps: one row
117
+ (copy, choices, close); copy above a row of three; copy above the three
118
+ stacked full width. A label wraps only at a space, never inside a word. */
116
119
  /* Room under the page while the bar is up (ConsentBanner sets the height); zero specificity, so a product body rule wins. */
117
120
  :where(body) { padding-block-end: var(--site-consent-block-size, 0px); }
118
- .site-consent { position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 900; max-block-size: 100dvh; overflow-y: auto; border-block-start: 1px solid var(--dds-color-border-default); background: var(--dds-color-bg-elevated); color: var(--dds-color-text-primary); box-shadow: var(--dds-elevation-3); }
119
- .site-consent__inner { inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; padding-block: var(--dds-space-16); display: grid; grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "copy actions close"; align-items: center; gap: var(--dds-space-12) var(--dds-space-24); }
121
+ .site-consent { container: site-consent / inline-size; position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 900; max-block-size: 100dvh; overflow-y: auto; border-block-start: 1px solid var(--dds-color-border-default); background: var(--dds-color-bg-elevated); color: var(--dds-color-text-primary); box-shadow: var(--dds-elevation-3); }
122
+ .site-consent__inner { inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; padding-block: var(--dds-space-16); display: grid; grid-template-columns: minmax(0, 1fr) fit-content(50%) auto; grid-template-areas: "copy actions close"; align-items: center; gap: var(--dds-space-12) var(--dds-space-24); }
120
123
  .site-consent__copy { grid-area: copy; display: grid; gap: var(--dds-space-4); min-inline-size: 0; }
121
124
  .site-consent__title { margin: 0; font-size: var(--dds-typo-body-1-font-size); line-height: var(--dds-typo-body-1-line-height); font-weight: 700; }
122
125
  .site-consent__body { margin: 0; max-inline-size: 72ch; font-size: var(--dds-typo-body-2-font-size); line-height: var(--dds-typo-body-2-line-height); color: var(--dds-color-text-secondary); }
123
126
  .site-consent__link { color: var(--dds-color-text-brand); text-decoration: underline; text-underline-offset: 2px; }
124
127
  .site-consent__link:focus-visible, .site-consent-trigger:focus-visible { outline: 2px solid var(--dds-color-border-focus); outline-offset: 2px; border-radius: var(--dds-radius-sm); }
125
128
  .site-consent__actions { grid-area: actions; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--dds-space-8); }
126
- .site-consent__action { inline-size: 100%; min-inline-size: 7rem; }
129
+ /* dds.css lets a button label break anywhere (overflow-wrap: anywhere); here a
130
+ line ends only at a space. Korean keeps whole words, whatever the page root says. */
131
+ .site-consent__action { inline-size: 100%; min-inline-size: 7rem; overflow-wrap: normal; word-break: normal; text-align: center; }
132
+ .site-consent__action:lang(ko) { word-break: keep-all; }
127
133
  .site-consent__dismiss { grid-area: close; align-self: start; }
128
- .dds-dialog.site-consent-dialog { position: relative; max-inline-size: 36rem; max-block-size: calc(100dvh - 2 * var(--dds-space-16)); overflow-y: auto; }
134
+ .dds-dialog.site-consent-dialog { container: site-consent-dialog / inline-size; position: relative; max-inline-size: 36rem; max-block-size: calc(100dvh - 2 * var(--dds-space-16)); overflow-y: auto; }
129
135
  .site-consent-dialog .dds-dialog__title { padding-inline-end: var(--dds-space-40); }
130
136
  .site-consent-dialog__close { position: absolute; inset-block-start: var(--dds-space-12); inset-inline-end: var(--dds-space-12); }
131
137
  .site-consent-categories { display: grid; margin: 0 0 var(--dds-space-12); padding: 0; list-style: none; }
@@ -137,18 +143,24 @@
137
143
  .site-consent-category__status, .site-consent-category__switch { justify-self: end; }
138
144
  .site-consent-category__status { background: var(--dds-color-bg-subtle); color: var(--dds-color-text-secondary); }
139
145
  .site-consent-dialog__policy { margin: 0 0 var(--dds-space-16); font-size: var(--dds-typo-body-2-font-size); }
140
- .site-consent-dialog .dds-dialog__actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
146
+ .site-consent-dialog .dds-dialog__actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
141
147
  /* The footer's "쿠키 설정" reads as one of its links. */
142
148
  .site-consent-trigger { appearance: none; margin: 0; padding: 0; border: 0; background: none; font: inherit; color: var(--dds-color-text-secondary); cursor: pointer; }
143
149
  .site-consent-trigger:hover { color: var(--dds-color-text-primary); text-decoration: underline; }
144
- @media (max-width: 720px) {
150
+ /* One row only while the copy keeps a readable measure beside the choices
151
+ (fit-content caps them at half the bar; their labels then wrap at spaces). */
152
+ @container site-consent (max-width: 60rem) {
145
153
  .site-consent__inner { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "copy close" "actions actions"; align-items: start; }
146
154
  .site-consent__actions { grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); }
147
155
  .site-consent__action { min-inline-size: 0; }
148
156
  }
149
- @media (max-width: 480px) {
150
- .site-consent-dialog .dds-dialog__actions { grid-template-columns: minmax(0, 1fr); }
157
+ /* A phone: a third of the bar is too narrow for a long word (Tamil, Telugu,
158
+ German), so the choices stack — full width, every row as tall as the tallest. */
159
+ @container site-consent (max-width: 36rem) {
160
+ .site-consent__actions { grid-template-columns: minmax(0, 1fr); grid-auto-rows: 1fr; }
151
161
  }
152
- @media (max-width: 340px) {
153
- .site-consent__actions { grid-template-columns: minmax(0, 1fr); }
162
+ /* The same in the settings dialog. A container query measures the content box:
163
+ the dialog's 36rem less its padding is 536px. */
164
+ @container site-consent-dialog (max-width: 32rem) {
165
+ .site-consent-dialog .dds-dialog__actions { grid-template-columns: minmax(0, 1fr); grid-auto-rows: 1fr; }
154
166
  }