File size: 6,247 Bytes
15d6beb
54a7dba
 
9ec7404
15d6beb
92cf18f
 
 
15d6beb
9ec7404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15d6beb
9ec7404
 
92cf18f
15d6beb
 
9ec7404
15d6beb
 
 
 
 
 
 
 
 
 
9ec7404
 
15d6beb
 
9ec7404
54a7dba
 
 
 
2d39154
54a7dba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ec7404
10c5bcc
 
9ec7404
10c5bcc
 
 
 
 
54a7dba
 
10c5bcc
 
 
 
 
54a7dba
 
 
10c5bcc
54a7dba
10c5bcc
54a7dba
 
 
 
10c5bcc
 
 
 
 
 
 
 
 
 
 
9ec7404
10c5bcc
 
 
54a7dba
 
 
15d6beb
 
 
 
444a402
15d6beb
 
54a7dba
 
 
444a402
 
 
54a7dba
 
 
 
 
 
 
 
9ec7404
54a7dba
7c1922e
54a7dba
15d6beb
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import type { Metadata, Viewport } from "next";
import { readFileSync } from "node:fs";
import path from "node:path";
import { Bricolage_Grotesque, Spline_Sans_Mono } from "next/font/google";
import "./globals.css";
import { readEngineInfo } from "@/lib/engine-info";

const engine = readEngineInfo();

// Fonts are fetched once at build time and served from this site, so there is no
// request to Google at runtime. Bricolage Grotesque does everything a person
// reads, Spline Sans Mono is only ever code and file names.
const display = Bricolage_Grotesque({
  subsets: ["latin"],
  variable: "--font-display",
  axes: ["opsz", "wdth"],
  display: "swap",
});
const mono = Spline_Sans_Mono({
  subsets: ["latin"],
  variable: "--font-code",
  display: "swap",
});

export const metadata: Metadata = {
  metadataBase: new URL("https://dssl-sable.vercel.app"),
  title: { default: "DSSL: a style language that tells you what you got wrong", template: "%s | DSSL" },
  description: `DSSL is a styling language for the web: design tokens, its own cascade, loud errors and zero specificity wars. The engine is ${engine.gzKb} kB gzipped, with no build step.`,
  keywords: ["DSSL", "CSS alternative", "style language", "design tokens", "web"],
  icons: { icon: "/favicon.svg" },
  openGraph: { type: "website", siteName: "DSSL", title: "DSSL: a style language that tells you what you got wrong" },
};

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
};

// The ONLY non-DSSL styling in this app: a pre-paint shim so the very first
// frame doesn't flash white before the DSSL engine boots (dark-mode respect).
const prePaint = `
  html { background: #f2f0f7; }
  @media (prefers-color-scheme: dark) { html { background: #130f1b; } }
`;

// v0.3.2, performance: the stylesheets ride INSIDE the HTML. The engine's
// auto-boot picks these <style type="text/dssl"> tags up the moment the module
// executes (right after parse, before first paint), so there is no fetch
// waterfall and no wait for React hydration. Server-side read also means a
// browser refresh picks up .dssl edits instantly in dev.
const DSSL_SHEETS = ["tokens", "site", "home", "docs", "playground", "changelog", "bench"] as const;

function dsslInlineStyles(): { name: string; src: string }[] {
  return DSSL_SHEETS.map((name) => {
    let src = "";
    try {
      src = readFileSync(path.join(process.cwd(), "public", "styles", `${name}.dssl`), "utf8");
    } catch {
      // missing sheet: boot simply won't apply it; errors stay loud elsewhere
    }
    // defensive: a "</style" sequence in source would break out of the tag
    src = src.replace(/<\/style/gi, "<\\/style");
    return { name, src };
  });
}

// Dev-only interop shim: the engine styles the SSR DOM BEFORE React hydrates
// (that is the whole point, no FOUC). React 19 then logs one console.error
// about extra style attributes and, helpfully, does not patch them. Next's
// dev overlay ALSO patches console.error (one layer around everything) and
// counts every error it sees into the "Issues" badge, so a plain filter
// installed here would sit INSIDE Next's collector and the badge would still
// show "1". This shim instead installs an assignment trap: the moment Next
// assigns its own wrapper, we capture it and re-publish OUR filter as the
// outermost layer. DSSL's own hydration diff is swallowed + explained; every
// other console.error still reaches Next's collector untouched.
const hydrationShim = `
(function () {
  var HYD = 'A tree hydrated but some attributes';
  var MARK = '--dssl';
  var inner = console.error;   // native, until Next assigns its wrapper
  var trapped = false;
  function outer() {
    var s = '';
    for (var i = 0; i < arguments.length; i++) {
      var a = arguments[i];
      s += (i ? ' ' : '') + (typeof a === 'string' ? a : (a && a.message ? a.message : ''));
    }
    if (s.indexOf(HYD) !== -1 && s.indexOf(MARK) !== -1) {
      console.info('%cDSSL%c styled this page before React hydrated. The "hydrated but some attributes didn\\u2019t match" notice is expected interop: the engine owns those style attributes, and React explicitly leaves them alone.',
        'background:#b3372e;color:#fff;padding:1px 6px;border-radius:3px;font-weight:700', '');
      return;
    }
    return inner.apply(console, arguments);
  }
  try {
    var desc = Object.getOwnPropertyDescriptor(console, 'error');
    if (!desc || !desc.configurable) throw new Error('not configurable');
    Object.defineProperty(console, 'error', {
      configurable: true,
      get: function () { return trapped ? outer : inner; },
      set: function (fn) { inner = fn; trapped = true; }
    });
  } catch (e) {
    // fallback: plain filter, console noise is still explained away,
    // though the dev-overlay badge may still count the notice.
    console.error = outer;
  }
})();
`;

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${display.variable} ${mono.variable}`} suppressHydrationWarning>
      <head>
        <style dangerouslySetInnerHTML={{ __html: prePaint }} />
        {process.env.NODE_ENV !== "production" && (
          <script dangerouslySetInnerHTML={{ __html: hydrationShim }} />
        )}
        {/* the same sheets, compiled to plain CSS by `dssl build` (scripts/build-site-css.mjs): styled on first paint, with
            JavaScript off, and in the server-rendered HTML. The engine then adds what a build cannot keep. The reset is in it. */}
        <link rel="stylesheet" href="/site.css" />
        {dsslInlineStyles().map(({ name, src }) => (
          <style
            key={name}
            type="text/dssl"
            data-dssl-name={`${name}.dssl`}
            dangerouslySetInnerHTML={{ __html: src }}
          />
        ))}
        {/* engine starts downloading with the document, module defer rules
            mean it executes after parse with the full SSR DOM present */}
        {/* eslint-disable-next-line @next/next/no-sync-scripts -- type="module" is deferred by spec; the rule cannot see that */}
        <script type="module" src="/dssl/boot.js" />
      </head>
      <body>{children}</body>
    </html>
  );
}