gristlabs / grist-core

Grist is the evolution of spreadsheets.
https://www.getgrist.com/
Apache License 2.0
7.1k stars 316 forks source link

Styling custom widgets like the main Grist UI #552

Open allquixotic opened 1 year ago

allquixotic commented 1 year ago

My custom widget should be styled with the same appearance as the main Grist UI, including support for dark mode. I can load the same CSS files in my widget as the enclosing Grist app, but I'm not sure how to handle dark mode support in my own widget HTML/CSS.

Are the class names like _grain12_ _grain12_-primary supposed to be stable, or do those numbers after the word "grain" (12, 427, 513, etc.) change with Grist versions? Just trying to figure out, if I want to have input fields (buttons, text boxes, text areas, etc.) within my custom widget, how to make them look the same as the rest of the Grist app.

Kinda worried that if I hard-code the class names in my HTML, it will only support the specific "mode" I'm using right now (dark vs. light mode).

paulfitz commented 1 year ago

Good question @allquixotic. @georgegevoian @dsagal do you have any thoughts on what would be a good way to achieve a decent level of consistency in the styling for custom widgets, and specifically to handle dark/light mode? We could do with that for custom widgets developed by Grist Labs as well. We could bundle some css for use with custom widgets, but I'm not sure exactly what form that would most usefully take.

allquixotic commented 1 year ago

I modified the onOptions.html sample with the following at the top; text boxes now render consistently with the Grist UI, and the background of the iframe goes dark when dark mode is enabled, but the button on the options pane still renders with the default style of the web browser. The jQuery script at the bottom of the snippet below shows how much tedious work it would be to set up equivalent styles for all the typical input fields.

NB: This assumes your custom widget is on the same domain as your app. Not even sure how this is possible otherwise.

<head>
  <meta charset="utf-8">
  <title>onOptions</title>
  <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
  <link rel="stylesheet" href="/jqueryui/themes/smoothness/jquery-ui.css">
  <link rel="stylesheet" href="/bootstrap/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="/hljs.default.css">
  <link rel="stylesheet" href="/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css">
  <link rel="stylesheet" href="/bundle.css">
  <link rel="stylesheet" href="/icons/icons.css">
  <script>
    try {
      if (localStorage.getItem('appearance') === 'dark') {
        const style = document.createElement('style');
        style.setAttribute('id', 'grist-theme');
        style.textContent = ':root {\n' +
          '  --grist-theme-bg: url("/img/prismpattern.png");\n' +
          '  --grist-theme-bg-color: #333333;\n' +
          '}';
        document.head.append(style);
        document.documentElement.style = "color-scheme: dark";
        document.documentElement.className = document.documentElement.className + " _grain3_ _grain5_";
      }
    } catch {
      /* Do nothing. */
    }
    $(document).ready(function () {
      $('button').addClass('_grain12_ _grain12_-primary');
      $('input[type="text"]').addClass('_grain427_ test-right-widget-title _grain513_');
    });
  </script>
</head>
georgegevoian commented 1 year ago

@allquixotic the class names will not be stable - we use a CSS-in-JS approach for most of our styling, and those names are what get generated when the app is built.

For simple components like text inputs and buttons, we could bundle CSS with stable class names as you suggested @paulfitz, but I wonder what we would do about more complex components like dropdown menus. We could externalize those into a library, but I suspect they'd need to be usable within React, Angular, Vue, etc., so it would require significant planning and work. There might be other approaches too - haven't thought about this much.

For light/dark mode integration, the way it works now is a style element is added to the document root with a list of CSS custom properties corresponding to various UI elements (e.g. text, inputs, buttons, etc.). Those properties can't currently be accessed within iframes, but I suspect we can figure out some way to share those properties via the API (perhaps by duplicating them within each iframe, and keeping it in sync with the version in the main document - we'd need to try this and see if there are any gotchas).

dsagal commented 1 year ago

I could imagine adding a bit to the grainjs styled function (that implements CSS-in-JS approach) to create "exportable" css classes with well-known (rather than generated) names. We could then have a tool that prepares a CSS file with all these exportable css classes. We can include this CSS file in our releases, and we can add a way for iframes to get the URL of the current version of this file, and perhaps with a convenience method like loadGristCss().

I could see it being helpful for some basic things, like text, buttons, icons, and all the CSS variables for light/dark colors.

For making a custom widget that looks like the app, there are many UI pieces that need more than CSS, but components. We use grainjs for the main app, and may use it also for more complex custom widgets. We've only really ever used the reusable components in the context of one big app with a single build process. We could create a JS bundle of some common components, and include it in our releases. Not sure how close that would get us to a convenient reuse of Grist styles in custom widgets.

allquixotic commented 1 year ago

My use case for this, from an application design perspective, is to create custom input forms.

Essentially, I have a bunch of tables that are linked together by some primary keys (like a database, except that in Grist the type is Reference), and a lot of the fields are optional or not needed at all in specific workflows.

I want to provide the user a "clean" view of the fields they need to enter, possibly with some client-side JS to help with pre-processing their data, etc., and then have the backend insert rows into the relevant tables under the hood.

So, if the look and feel of my custom widget is the same as the Grist UI, it'll look "native" rather than a tacked-in iframe :)

I know you guys have "forms" on your backlog (something like what MS Access provides?) but it isn't there now, and I need to make Grist work now-ish, so... :)

paulfitz commented 1 year ago

@JakubSerafin @georgegevoian what do you think of @dsagal's suggestion of a css bundle, to parallel the js bundle we already prepare for custom widgets? Are there alternatives? How exactly would it work? @JakubSerafin I think this relates to some work you are doing on supporting dark mode for a calendar custom widget.

georgegevoian commented 1 year ago

Sounds reasonable to me.

I expect it would work like Bootstrap where developers pick from a set of pre-defined CSS classes to apply to elements (buttons, inputs, etc.). Bootstrap also makes colors available globally through CSS custom properties, but in our case the variables change dynamically based on the current theme; they would need to be set and kept in sync with the variables in the main Grist app, which I think this is one of the things @JakubSerafin is looking into.